diff --git a/.gitignore b/.gitignore index 1ae33b8..8043054 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ _ReSharper.Caches /YololEmulator.Tests/*.ncrunchproject /Yololc/Properties/PublishProfiles/*.pubxml /Yolol/nuget-push.ps1 +/Yolol/*.ncrunchproject diff --git a/Yolol.Cylon/Serialisation/AstSerializer.cs b/Yolol.Cylon/Serialisation/AstSerializer.cs index 52b6a3d..f2ec85e 100644 --- a/Yolol.Cylon/Serialisation/AstSerializer.cs +++ b/Yolol.Cylon/Serialisation/AstSerializer.cs @@ -108,6 +108,7 @@ private JToken SerializeExpression(BaseExpression expr) LessThanEqualTo cmp => SerializeBinary(cmp, "less_than_or_equal_to"), EqualTo cmp => SerializeBinary(cmp, "equal_to"), NotEqualTo cmp => SerializeBinary(cmp, "not_equal_to"), + Sqrt sqrt => new JObject {["type"] = "expression::unary_op::sqrt", ["operand"] = SerializeExpression(sqrt.Parameter)}, _ => throw new NotSupportedException($"Cannot serialize expression type `{expr.GetType().Name}`") }; } diff --git a/Yolol/Grammar/AST/Expressions/Unary/Abs.cs b/Yolol/Grammar/AST/Expressions/Unary/Abs.cs index bc40955..731f273 100644 --- a/Yolol/Grammar/AST/Expressions/Unary/Abs.cs +++ b/Yolol/Grammar/AST/Expressions/Unary/Abs.cs @@ -10,8 +10,6 @@ public class Abs public override bool IsBoolean => false; - public override bool IsConstant => Parameter.IsConstant; - public Abs(BaseExpression parameter) : base(parameter) { @@ -33,7 +31,7 @@ public override bool Equals(BaseExpression? other) public override string ToString() { - return $"ABS({Parameter})"; + return $"ABS {Parameter}"; } } } diff --git a/Yolol/Grammar/AST/Expressions/Unary/BaseTrigonometry.cs b/Yolol/Grammar/AST/Expressions/Unary/BaseTrigonometry.cs index a8a450f..f1d079c 100644 --- a/Yolol/Grammar/AST/Expressions/Unary/BaseTrigonometry.cs +++ b/Yolol/Grammar/AST/Expressions/Unary/BaseTrigonometry.cs @@ -16,7 +16,7 @@ protected BaseTrigonometry(BaseExpression parameter, string name) public override string ToString() { - return $"{_name}({Parameter})"; + return $"{_name} {Parameter}"; } } } diff --git a/Yolol/Grammar/AST/Expressions/Unary/Factorial.cs b/Yolol/Grammar/AST/Expressions/Unary/Factorial.cs new file mode 100644 index 0000000..33002ef --- /dev/null +++ b/Yolol/Grammar/AST/Expressions/Unary/Factorial.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Yolol.Execution; + +namespace Yolol.Grammar.AST.Expressions.Unary +{ + public class Factorial + : BaseUnaryExpression + { + public override bool IsBoolean => false; + + public override bool CanRuntimeError => true; + + public Factorial(BaseExpression parameter) + : base(parameter) + { + } + + + public bool Equals(Factorial? other) + { + return other != null + && other.Parameter.Equals(Parameter); + } + + public override bool Equals(BaseExpression? other) + { + return other is Factorial fac + && fac.Equals(this); + } + + public override string ToString() + { + return $"{Parameter}!"; + } + + protected override Value Evaluate(Value value) + { + if (value.Type == Execution.Type.String) + throw new ExecutionException("Attempted to apply factorial to a string"); + else + throw new NotImplementedException(); + } + } +} diff --git a/Yolol/Grammar/AST/Expressions/Unary/Sqrt.cs b/Yolol/Grammar/AST/Expressions/Unary/Sqrt.cs index 3d60cd4..df589d4 100644 --- a/Yolol/Grammar/AST/Expressions/Unary/Sqrt.cs +++ b/Yolol/Grammar/AST/Expressions/Unary/Sqrt.cs @@ -33,7 +33,7 @@ public override bool Equals(BaseExpression? other) public override string ToString() { - return $"SQRT({Parameter})"; + return $"SQRT {Parameter}"; } } } diff --git a/Yolol/Grammar/Parser.cs b/Yolol/Grammar/Parser.cs index 861bebc..434291e 100644 --- a/Yolol/Grammar/Parser.cs +++ b/Yolol/Grammar/Parser.cs @@ -1,196 +1,83 @@ using System; -using Superpower; -using Superpower.Model; -using Superpower.Parsers; +using Pegasus.Common; using Yolol.Grammar.AST; -using Yolol.Grammar.AST.Expressions; -using Yolol.Grammar.AST.Expressions.Binary; -using Yolol.Grammar.AST.Expressions.Unary; -using Yolol.Grammar.AST.Statements; namespace Yolol.Grammar { - public static class Parser + public class Parser { - private static readonly TokenListParser Add = Token.EqualTo(YololToken.Plus).Value(YololBinaryOp.Add); - private static readonly TokenListParser Subtract = Token.EqualTo(YololToken.Subtract).Value(YololBinaryOp.Subtract); - private static readonly TokenListParser Multiply = Token.EqualTo(YololToken.Multiply).Value(YololBinaryOp.Multiply); - private static readonly TokenListParser Divide = Token.EqualTo(YololToken.Divide).Value(YololBinaryOp.Divide); - private static readonly TokenListParser Modulo = Token.EqualTo(YololToken.Modulo).Value(YololBinaryOp.Modulo); - private static readonly TokenListParser Exponent = Token.EqualTo(YololToken.Exponent).Value(YololBinaryOp.Exponent); - - private static readonly TokenListParser And = Token.EqualTo(YololToken.And).Value(YololBinaryOp.And); - private static readonly TokenListParser Or = Token.EqualTo(YololToken.Or).Value(YololBinaryOp.Or); - - private static readonly TokenListParser CompoundAdd = Token.EqualTo(YololToken.CompoundPlus).Value(YololBinaryOp.Add); - private static readonly TokenListParser CompoundSubtract = Token.EqualTo(YololToken.CompoundSubtract).Value(YololBinaryOp.Subtract); - private static readonly TokenListParser CompoundMultiply = Token.EqualTo(YololToken.CompoundMultiply).Value(YololBinaryOp.Multiply); - private static readonly TokenListParser CompoundDivide = Token.EqualTo(YololToken.CompoundDivide).Value(YololBinaryOp.Divide); - private static readonly TokenListParser CompoundModulo = Token.EqualTo(YololToken.CompoundModulo).Value(YololBinaryOp.Modulo); - - private static readonly TokenListParser LessThan = Token.EqualTo(YololToken.LessThan).Value(YololBinaryOp.LessThan); - private static readonly TokenListParser GreaterThan = Token.EqualTo(YololToken.GreaterThan).Value(YololBinaryOp.GreaterThan); - private static readonly TokenListParser LessThanEqualTo = Token.EqualTo(YololToken.LessThanEqualTo).Value(YololBinaryOp.LessThanEqualTo); - private static readonly TokenListParser GreaterThanEqualTo = Token.EqualTo(YololToken.GreaterThanEqualTo).Value(YololBinaryOp.GreaterThanEqualTo); - private static readonly TokenListParser NotEqualTo = Token.EqualTo(YololToken.NotEqualTo).Value(YololBinaryOp.NotEqualTo); - private static readonly TokenListParser EqualTo = Token.EqualTo(YololToken.EqualTo).Value(YololBinaryOp.EqualTo); - - private static readonly TokenListParser Not = - from not in Token.EqualTo(YololToken.Not) - from exp in Parse.Ref(() => Expression) - select (BaseExpression)new Not(exp); - - private static readonly TokenListParser VariableName = Token.EqualTo(YololToken.Identifier).Select(n => new VariableName(n.ToStringValue())); - private static readonly TokenListParser ExternalVariableName = Token.EqualTo(YololToken.ExternalIdentifier).Select(n => new VariableName(n.ToStringValue())); - - private static readonly TokenListParser PositiveNumExpression = Token.EqualTo(YololToken.Number).Select(n => (BaseExpression)new ConstantNumber(decimal.Parse(n.ToStringValue()))); - private static readonly TokenListParser NegativeNumExpression = - from neg in Token.EqualTo(YololToken.Subtract) - from num in Token.EqualTo(YololToken.Number) - select (BaseExpression)new ConstantNumber(-decimal.Parse(num.ToStringValue())); - - private static readonly TokenListParser ConstantNumExpression = PositiveNumExpression.Or(NegativeNumExpression); - - private static readonly TokenListParser ConstantStrExpression = Token.EqualTo(YololToken.String).Select(n => (BaseExpression)new ConstantString(n.ToStringValue().Trim('"'))); - private static readonly TokenListParser VariableExpression = from name in VariableName select (BaseExpression)new Variable(name); - private static readonly TokenListParser ExternalVariableExpression = from name in ExternalVariableName select (BaseExpression)new Variable(name); - - private static readonly TokenListParser PreIncrementExpr = - from inc in Token.EqualTo(YololToken.Increment) - from var in VariableName - select (BaseExpression)new PreIncrement(var); - - private static readonly TokenListParser PostIncrementExpr = - from var in VariableName - from inc in Token.EqualTo(YololToken.Increment) - select (BaseExpression)new PostIncrement(var); - - private static readonly TokenListParser PreDecrementExpr = - from dec in Token.EqualTo(YololToken.Decrement) - from var in VariableName - select (BaseExpression)new PreDecrement(var); - - private static readonly TokenListParser PostDecrementExpr = - from var in VariableName - from dec in Token.EqualTo(YololToken.Decrement) - select (BaseExpression)new PostDecrement(var); - - private static readonly TokenListParser PreIncrementStat = PreIncrementExpr.Select(a => (BaseStatement)new ExpressionWrapper(a)); - private static readonly TokenListParser PostIncrementStat = PostIncrementExpr.Select(a => (BaseStatement)new ExpressionWrapper(a)); - private static readonly TokenListParser PreDecrementStat = PreDecrementExpr.Select(a => (BaseStatement)new ExpressionWrapper(a)); - private static readonly TokenListParser PostDecrementStat = PostDecrementExpr.Select(a => (BaseStatement)new ExpressionWrapper(a)); - - private static TokenListParser MkFunction(YololToken token, Func make) + public static Result ParseProgram(string program) { - return from nm in Token.EqualTo(token) - from lp in Token.EqualTo(YololToken.LParen) - from exp in Expression - from rp in Token.EqualTo(YololToken.RParen) - select make(exp); + try + { + var p = new YololParser(); + return new Result(p.Parse(program)); + } + catch (FormatException e) + { + var c = (Cursor)e.Data["cursor"]; + return new Result(new ParseError(c, e.Message)); + } } - private static readonly TokenListParser SqrtExpression = MkFunction(YololToken.Sqrt, a => new Sqrt(a)); - private static readonly TokenListParser AbsExpression = MkFunction(YololToken.Abs, a => new Abs(a)); - private static readonly TokenListParser SineExpression = MkFunction(YololToken.Sine, a => new Sine(a)); - private static readonly TokenListParser CosineExpression = MkFunction(YololToken.Cosine, a => new Cosine(a)); - private static readonly TokenListParser TangentExpression = MkFunction(YololToken.Tangent, a => new Tangent(a)); - private static readonly TokenListParser ArcSineExpression = MkFunction(YololToken.ArcSin, a => new ArcSine(a)); - private static readonly TokenListParser ArcCosExpression = MkFunction(YololToken.ArcCos, a => new ArcCos(a)); - private static readonly TokenListParser ArcTanExpression = MkFunction(YololToken.ArcTan, a => new ArcTan(a)); - - private static readonly TokenListParser FunctionCall = - SqrtExpression.Try() - .Or(AbsExpression.Try()) - .Or(SineExpression.Try()) - .Or(CosineExpression.Try()) - .Or(TangentExpression.Try()) - .Or(ArcSineExpression.Try()) - .Or(ArcCosExpression.Try()) - .Or(ArcTanExpression.Try()); - - private static readonly TokenListParser Factor = - (from lparen in Token.EqualTo(YololToken.LParen) - from expr in Parse.Ref(() => Expression) - from rparen in Token.EqualTo(YololToken.RParen) - select (BaseExpression)new Bracketed(expr)) - .Or(FunctionCall.Try()) - .Or(PostDecrementExpr.Try()) - .Or(PreIncrementExpr.Try()) - .Or(PostIncrementExpr.Try()) - .Or(PreDecrementExpr.Try()) - .Or(ConstantNumExpression.Try()) - .Or(ConstantStrExpression.Try()) - .Or(VariableExpression.Try()) - .Or(ExternalVariableExpression.Try()); - - private static readonly TokenListParser Operand = - ConstantNumExpression - .Try().Or(from sign in Token.EqualTo(YololToken.Subtract) - from factor in Factor - select (BaseExpression)new Negate(factor)) - .Or(Not.Try()) - .Or(Factor.Try()) - .Named("expression"); - - private static readonly TokenListParser Term = - Parse.Chain(Multiply.Or(Divide).Or(Modulo).Or(Exponent), Operand, BaseBinaryExpression.Create).Try(); - - private static readonly TokenListParser Expression = - Parse.Chain(Add.Or(Subtract).Or(LessThan).Or(GreaterThan).Or(LessThanEqualTo).Or(GreaterThanEqualTo).Or(NotEqualTo).Or(EqualTo).Or(And).Or(Or), Term, BaseBinaryExpression.Create); - - private static readonly TokenListParser Assignment = - from lhs in VariableName.Or(ExternalVariableName) - from op in Token.EqualTo(YololToken.Assignment) - from rhs in Expression - select (BaseStatement)new Assignment(lhs, rhs); - - private static readonly TokenListParser CompoundAssignment = - from lhs in VariableName.Or(ExternalVariableName) - from op in CompoundAdd.Or(CompoundSubtract.Try()).Or(CompoundMultiply.Try()).Or(CompoundDivide.Try()).Or(CompoundModulo.Try()) - from rhs in Expression - select (BaseStatement)new CompoundAssignment(lhs, op, rhs); - - private static readonly TokenListParser Goto = - from @goto in Token.EqualTo(YololToken.Goto) - from destination in Expression - select (BaseStatement)new Goto(destination); - - private static readonly TokenListParser If = - from @if in Token.EqualTo(YololToken.If) - from cond in Expression - from then in Token.EqualTo(YololToken.Then) - from trueBranch in Parse.Ref(() => Statement.Many()) - from falseBranch in (Token.EqualTo(YololToken.Else).IgnoreThen(Parse.Ref(() => Statement.Many())).OptionalOrDefault(Array.Empty())) - from end in Token.EqualTo(YololToken.End) - select (BaseStatement)new If(cond, new StatementList(trueBranch), new StatementList(falseBranch)); - - private static readonly TokenListParser Statement = - Assignment.Try() - .Or(If.Try()) - .Or(CompoundAssignment.Try()) - .Or(Goto.Try()) - .Or(PreIncrementStat.Try()) - .Or(PreDecrementStat.Try()) - .Or(PostIncrementStat.Try()) - .Or(PostDecrementStat); - - private static readonly TokenListParser Line = - from statements in Statement.Many() - from nl in Token.EqualTo(YololToken.NewLine) - select new Line(new StatementList(statements)); - - private static readonly TokenListParser Program = - from lines in Line.Many().AtEnd() - select new Program(lines); - - public static TokenListParserResult TryParseLine(TokenList tokens) + public readonly struct Result + where TOk : class + where TErr : class { - return Line.TryParse(tokens); + private readonly TOk? _ok; + private readonly TErr? _err; + + public TOk Ok + { + get + { + if (IsOk) + return _ok!; + throw new InvalidOperationException("Cannot get an ok value from an error result"); + } + } + + public TErr Err { + get + { + if (!IsOk) + return _err!; + throw new InvalidOperationException("Cannot get an error value from ok result"); + } + } + + public bool IsOk { get; } + + public Result(TOk ok) + { + IsOk = true; + _ok = ok; + _err = null; + } + + public Result(TErr err) + { + IsOk = false; + _ok = null; + _err = err; + } } - public static TokenListParserResult TryParseProgram(TokenList tokens) + public class ParseError { - return Program.TryParse(tokens); + public Cursor Cursor { get; } + public string Message { get; } + + public ParseError(Cursor cursor, string message) + { + Cursor = cursor; + Message = message; + } + + public override string ToString() + { + return $"{Message} (Ln{Cursor.Line}, Ch{Cursor.Column})"; + } } } } diff --git a/Yolol/Grammar/Tokenizer.cs b/Yolol/Grammar/Tokenizer.cs deleted file mode 100644 index 8d535f8..0000000 --- a/Yolol/Grammar/Tokenizer.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Superpower; -using Superpower.Model; -using Superpower.Parsers; -using Superpower.Tokenizers; - -namespace Yolol.Grammar -{ - public class Tokenizer - { - private static Tokenizer Instance { get; } = - new TokenizerBuilder() - - .Match(Span.Regex("\\r?\\n"), YololToken.NewLine) - - .Ignore(Span.WhiteSpace) - .Ignore(Comment.CPlusPlusStyle) - - .Match(Span.EqualTo("if"), YololToken.If) - .Match(Span.EqualTo("then"), YololToken.Then) - .Match(Span.EqualTo("else"), YololToken.Else) - .Match(Span.EqualTo("end"), YololToken.End) - .Match(Span.EqualTo("goto"), YololToken.Goto) - - .Match(Span.EqualTo("and"), YololToken.And) - .Match(Span.EqualTo("or"), YololToken.Or) - .Match(Span.EqualTo("not"), YololToken.Not) - - .Match(Span.EqualToIgnoreCase("abs"), YololToken.Abs) - .Match(Span.EqualToIgnoreCase("sqrt"), YololToken.Sqrt) - .Match(Span.EqualToIgnoreCase("sin"), YololToken.Sine) - .Match(Span.EqualToIgnoreCase("cos"), YololToken.Cosine) - .Match(Span.EqualToIgnoreCase("tan"), YololToken.Tangent) - .Match(Span.EqualToIgnoreCase("asin"), YololToken.ArcSin) - .Match(Span.EqualToIgnoreCase("acos"), YololToken.ArcCos) - .Match(Span.EqualToIgnoreCase("atan"), YololToken.ArcTan) - - .Match(Span.EqualTo("<="), YololToken.LessThanEqualTo) - .Match(Span.EqualTo(">="), YololToken.GreaterThanEqualTo) - .Match(Character.EqualTo('<'), YololToken.LessThan) - .Match(Character.EqualTo('>'), YololToken.GreaterThan) - - .Match(Span.EqualTo("!="), YololToken.NotEqualTo) - .Match(Span.EqualTo("=="), YololToken.EqualTo) - - .Match(Span.EqualTo(':').IgnoreThen(Identifier.CStyle), YololToken.ExternalIdentifier) - .Match(Identifier.CStyle, YololToken.Identifier) - .Match(QuotedString.CStyle, YololToken.String) - - .Match(Character.EqualTo('='), YololToken.Assignment) - - .Match(Character.EqualTo('('), YololToken.LParen) - .Match(Character.EqualTo(')'), YololToken.RParen) - - .Match(Span.EqualTo("++"), YololToken.Increment) - .Match(Span.EqualTo("--"), YololToken.Decrement) - - .Match(Span.EqualTo("+="), YololToken.CompoundPlus) - .Match(Span.EqualTo("-="), YololToken.CompoundSubtract) - .Match(Span.EqualTo("*="), YololToken.CompoundMultiply) - .Match(Span.EqualTo("/="), YololToken.CompoundDivide) - .Match(Span.EqualTo("%="), YololToken.CompoundModulo) - - .Match(Character.EqualTo('+'), YololToken.Plus) - .Match(Character.EqualTo('-'), YololToken.Subtract) - .Match(Character.EqualTo('*'), YololToken.Multiply) - .Match(Character.EqualTo('/'), YololToken.Divide) - .Match(Character.EqualTo('%'), YololToken.Modulo) - .Match(Character.EqualTo('^'), YololToken.Exponent) - - .Match(Numerics.Decimal, YololToken.Number) - - .Build(); - - public static Result> TryTokenize(string str) - { - // Ensure the last line ends with a newline - if (!str.EndsWith("\n")) - str += "\n"; - - return Instance.TryTokenize(str); - } - } -} diff --git a/Yolol/Grammar/YololParser.pegasus b/Yolol/Grammar/YololParser.pegasus new file mode 100644 index 0000000..ecdb584 --- /dev/null +++ b/Yolol/Grammar/YololParser.pegasus @@ -0,0 +1,171 @@ +@namespace Yolol.Grammar +@classname YololParser +@accessibility internal +@using System.Globalization +@using Yolol.Grammar.AST +@using Yolol.Grammar.AST.Statements +@using Yolol.Grammar.AST.Expressions +@using Yolol.Grammar.AST.Expressions.Binary +@using Yolol.Grammar.AST.Expressions.Unary +@using Yolol.Execution; +@start program +@trace true + +program + = comment { new Program(Array.Empty()) } + / lines:line<0,, (comment / newline)> _ EOF { new Program(lines) } + +line -public + = stmts:statement<0,, _> { new Line(new StatementList(stmts)) } + +// ######################################## +// Statements + +statement + = goto + / modify + / if + / assign + / compound_assign + +goto + = "goto" _ e:expression { new Goto(e) } + +modify + = i:identifier "++" { new ExpressionWrapper(new PostIncrement(i)) } + / i:identifier "--" { new ExpressionWrapper(new PostDecrement(i)) } + / "++" i:identifier { new ExpressionWrapper(new PreIncrement(i)) } + / "--" i:identifier { new ExpressionWrapper(new PreDecrement(i)) } + +if + = "if" _ c:expression _ "then" _ t:statement<0,, _> _ "else" _ f:statement<0,, _> _ "end" + { new If(c, new StatementList(t), new StatementList(f)) } + / "if" _ c:expression _ "then" _ t:statement<0,, _> _ "end" + { new If(c, new StatementList(t), new StatementList()) } + +assign + = i:identifier _ "=" _ e:expression { new Assignment(i, e) } + +compound_assign + = i:identifier _ c:compound_op _ e:expression { new CompoundAssignment(i, c, e) } + +compound_op + = "+=" { YololBinaryOp.Add } + / "-=" { YololBinaryOp.Subtract } + / "*=" { YololBinaryOp.Multiply } + / "/=" { YololBinaryOp.Divide } + / "%=" { YololBinaryOp.Modulo } + / "^=" { YololBinaryOp.Exponent } + +// ######################################## +// Expressions + +expression + = expr_and + +expr_and -memoize + = l:expr_and _ "and" _ r:expr_or { new And(l, r) } + / e:expr_or { e } + +expr_or -memoize + = l:expr_or _ "or" _ r:expr_equality { new Or(l, r) } + / e:expr_equality { e } + +expr_equality -memoize + = l:expr_equality _ "==" _ r:expr_order { new EqualTo(l, r) } + / l:expr_equality _ "!=" _ r:expr_order { new NotEqualTo(l, r) } + / e:expr_order { e } + +expr_order -memoize + = l:expr_order _ "<" _ r:expr_additive { new LessThan(l, r) } + / l:expr_order _ ">" _ r:expr_additive { new GreaterThan(l, r) } + / l:expr_order _ "<=" _ r:expr_additive { new LessThanEqualTo(l, r) } + / l:expr_order _ ">=" _ r:expr_additive { new GreaterThanEqualTo(l, r) } + / e:expr_additive { e } + +expr_additive -memoize + = l:expr_additive _ "+" _ r:expr_multiply { new Add(l, r) } + / l:expr_additive _ "-" _ r:expr_multiply { new Subtract(l, r) } + / e:expr_multiply { e } + +expr_multiply -memoize + = l:expr_multiply _ "*" _ r:expr_exponent { new Multiply(l, r) } + / l:expr_multiply _ "/" _ r:expr_exponent { new Divide(l, r) } + / l:expr_multiply _ "%" _ r:expr_exponent { new Modulo(l, r) } + / e:expr_exponent { e } + +expr_exponent -memoize + = l:expr_postfix _ "^" _ r:expr_exponent { new Exponent(l, r) } + / e:expr_postfix { e } + +expr_postfix -memoize + // = e:expr_postfix "!" { new Factorial(e) } todo: support factorial + = e:expr_keyword { e } + +expr_keyword -memoize + = "abs"i _ e:expr_keyword { new Abs(e) } + / "sqrt"i _ e:expr_keyword { new Sqrt(e) } + / "sin"i _ e:expr_keyword { new Sine(e) } + / "cos"i _ e:expr_keyword { new Cosine(e) } + / "tan"i _ e:expr_keyword { new Tangent(e) } + / "asin"i _ e:expr_keyword { new ArcSine(e) } + / "acos"i _ e:expr_keyword { new ArcCos(e) } + / "atan"i _ e:expr_keyword { new ArcTan(e) } + / "not"i _ e:expr_keyword { new Not(e) } + / e:expr_neg { e } + +expr_neg -memoize + = e:expr_ident { e } + / "-" _ e:expr_neg { new Negate(e) } + +expr_ident -memoize + = i:identifier "++" { new PostIncrement(i) } + / i:identifier "--" { new PostDecrement(i) } + / "++" i:identifier { new PreIncrement(i) } + / "--" i:identifier { new PreDecrement(i) } + / value; + +value -memoize + = s:string { new ConstantString(s) } + / n:number { new ConstantNumber(n) } + / i:identifier { new Yolol.Grammar.AST.Expressions.Variable(i) } + / "(" _ e:expression _ ")" { new Bracketed(e) } + +// ######################################## +// Others + +identifier + = s:([a-zA-Z_][a-zA-Z0-9_]<0,>) { new VariableName(s) } + / ":" s:([a-zA-Z0-9_]<1,>) { new VariableName(":" + string.Concat(s)) } + +newline + = "\r\n" + / "\n" + +comment + = "//" (!newline .)* newline + +_ + = [ \t]* + +EOF + = !. + / unexpected:. #error{ "Unexpected character '" + unexpected + "'." } + +string + = "\"" chars:unicode* ("\"" / #ERROR{ "Expected '\"'" }) { string.Concat(chars) } + +unicode + = c:. !{c == "\\" || c == "\"" || char.IsControl(c[0])} { c } + / "\\" c:( + e:["\/\\] { e } / + "b" { "\b" } / + "f" { "\f" } / + "n" { "\n" } / + "r" { "\r" } / + "t" { "\t" } / + "u" digits:("" [0-9A-F]i<4>) { ((char)Convert.ToInt32(digits, 16)).ToString() } + ) { c } + +number + = s:("-"? [0-9]+ ("." [0-9]+)?) { (Number)decimal.Parse(s) } \ No newline at end of file diff --git a/Yolol/Yolol.csproj b/Yolol/Yolol.csproj index 5f2559a..4004d52 100644 --- a/Yolol/Yolol.csproj +++ b/Yolol/Yolol.csproj @@ -24,12 +24,17 @@ + - + + + + + diff --git a/YololEmulator.Tests/AST/Precedence.cs b/YololEmulator.Tests/AST/Precedence.cs new file mode 100644 index 0000000..fc0c3b6 --- /dev/null +++ b/YololEmulator.Tests/AST/Precedence.cs @@ -0,0 +1,138 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yolol.Grammar; +using Yolol.Grammar.AST; +using Yolol.Grammar.AST.Expressions; +using Yolol.Grammar.AST.Expressions.Binary; +using Yolol.Grammar.AST.Expressions.Unary; +using Yolol.Grammar.AST.Statements; +using Variable = Yolol.Grammar.AST.Expressions.Variable; + +namespace YololEmulator.Tests.AST +{ + [TestClass] + public class Precedence + { + // These tests each test an operator binds more strongly than the item immediately below it in this table: + // https://github.com/Jerald/yolol-is-cylons/blob/master/information/cylon_yolol.md + + [TestMethod] + public void Negate() + { + var actual = TestExecutor.Parse("a = -b * c"); + var expect = new Line(new StatementList(new Assignment(new VariableName("a"), new Multiply(new Negate(new Variable(new VariableName("b"))), new Variable(new VariableName("c")))))); + + Assert.IsTrue(expect.Equals(actual.Lines[0])); + } + + [TestMethod] + public void Increment() + { + var actual = TestExecutor.Parse("a = -b++"); + var expect = new Line(new StatementList(new Assignment(new VariableName("a"), new Negate(new PostIncrement(new VariableName("b")))))); + + Assert.IsTrue(expect.Equals(actual.Lines[0])); + } + + [TestMethod] + public void Keyword() + { + var actual = TestExecutor.Parse("a = sqrt b ^ c"); + var expect = new Line(new StatementList(new Assignment(new VariableName("a"), new Exponent(new Sqrt(new Variable(new VariableName("b"))), new Variable(new VariableName("c")))))); + + Assert.IsTrue(expect.Equals(actual.Lines[0])); + } + + [TestMethod] + public void ExponentAssociativity() + { + var state = TestExecutor.Execute("a = 2 ^ 2 ^ 3"); + Assert.AreEqual(256, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void ExponentMul() + { + var state = TestExecutor.Execute("a = 2 ^ 3 * 4"); + Assert.AreEqual(32, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void ExponentDiv() + { + var state = TestExecutor.Execute("a = 2 ^ 3 / 4"); + Assert.AreEqual(2, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void ExponentMod() + { + var state = TestExecutor.Execute("a = 2 ^ 3 % 5"); + Assert.AreEqual(3, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void MultiplyAdd() + { + var state = TestExecutor.Execute("a = 2 * 3 + 4"); + Assert.AreEqual(10, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void MultiplySub() + { + var state = TestExecutor.Execute("a = 2 * 3 - 4"); + Assert.AreEqual(2, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void AddLt() + { + var state = TestExecutor.Execute("a = 2 + 3 < 4"); + Assert.AreEqual(0, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void AddGt() + { + var state = TestExecutor.Execute("a = 2 + 3 > 4"); + Assert.AreEqual(1, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void AddLtEq() + { + var state = TestExecutor.Execute("a = 2 + 3 <= 4"); + Assert.AreEqual(0, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void AddGtEq() + { + var state = TestExecutor.Execute("a = 2 + 3 >= 4"); + Assert.AreEqual(1, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void GtEq() + { + var state = TestExecutor.Execute("a = 2 > 4 == 4"); + Assert.AreEqual(0, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void EqOr() + { + var state = TestExecutor.Execute("a = 4 == 4 or 0"); + Assert.AreEqual(1, state.GetVariable("a").Value.Number); + } + + [TestMethod] + public void AndOr() + { + var actual = TestExecutor.Parse("a = 1 and 2 or 3"); + var expect = new Line(new StatementList(new Assignment(new VariableName("a"), new And(new ConstantNumber(1), new Or(new ConstantNumber(2), new ConstantNumber(3)))))); + + Assert.IsTrue(expect.Equals(actual.Lines[0])); + } + } +} diff --git a/YololEmulator.Tests/AST/ProgramTests.cs b/YololEmulator.Tests/AST/ProgramTests.cs new file mode 100644 index 0000000..7a9e2c8 --- /dev/null +++ b/YololEmulator.Tests/AST/ProgramTests.cs @@ -0,0 +1,16 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace YololEmulator.Tests.AST +{ + [TestClass] + public class ProgramTests + { + [TestMethod] + public void CommentLine() + { + var ast = TestExecutor.Parse("// a = b"); + + Assert.AreEqual(0, ast.Lines.Count); + } + } +} diff --git a/YololEmulator.Tests/AST/ToString.cs b/YololEmulator.Tests/AST/ToString.cs index 0757e84..7cbe2ae 100644 --- a/YololEmulator.Tests/AST/ToString.cs +++ b/YololEmulator.Tests/AST/ToString.cs @@ -8,12 +8,11 @@ public class ToString { private static void Roundtrip(string line) { - var tok = Tokenizer.TryTokenize(line); - Assert.IsTrue(tok.HasValue); - var par = Parser.TryParseLine(tok.Value); - Assert.IsTrue(par.HasValue, par.FormatErrorMessageFragment()); + var result = Parser.ParseProgram(line + "\n"); - Assert.AreEqual(line, par.Value.ToString()); + if (!result.IsOk) + Assert.Fail(result.Err.ToString()); + Assert.AreEqual(line, result.Ok.ToString()); } [TestMethod] @@ -217,13 +216,13 @@ public void Brackets() [TestMethod] public void Sqrt() { - Roundtrip("a=SQRT(1<=a)"); + Roundtrip("a=SQRT 1<=a"); } [TestMethod] public void Abs() { - Roundtrip("a=ABS(3)"); + Roundtrip("a=ABS 3"); } } } diff --git a/YololEmulator.Tests/Analysis/Reduction/FlattenStatementListsTests.cs b/YololEmulator.Tests/Analysis/Reduction/FlattenStatementListsTests.cs index 587ef4c..a3261f2 100644 --- a/YololEmulator.Tests/Analysis/Reduction/FlattenStatementListsTests.cs +++ b/YololEmulator.Tests/Analysis/Reduction/FlattenStatementListsTests.cs @@ -14,7 +14,7 @@ public class FlattenStatementListsTests private static readonly ReducerTestHelper Helper = new ReducerTestHelper(ast => new FlattenStatementLists().Visit(ast)); [TestMethod] - public void Flatten() => Helper.Run("a=sin(b)", new Program(new Line[] { + public void Flatten() => Helper.Run("a=sin b", new Program(new Line[] { new Line(new StatementList(new Assignment(new VariableName("a"), new Sine(new Variable(new VariableName("b")))))) })); } diff --git a/YololEmulator.Tests/Scripts/LadderRepros.cs b/YololEmulator.Tests/Scripts/LadderRepros.cs new file mode 100644 index 0000000..85e8edd --- /dev/null +++ b/YololEmulator.Tests/Scripts/LadderRepros.cs @@ -0,0 +1,20 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace YololEmulator.Tests.Scripts +{ + [TestClass] + public class LadderRepros + { + [TestMethod] + public void MartinChallenge1() + { + TestExecutor.Parse(":d=:b+:c+:a:done=1goto1"); + } + + [TestMethod] + public void PyryChallenge1() + { + TestExecutor.Parse(":d=:b+:c+:a goto++:done"); + } + } +} diff --git a/YololEmulator.Tests/Scripts/NumberParsing.cs b/YololEmulator.Tests/Scripts/NumberParsing.cs index 58094d2..f7101e0 100644 --- a/YololEmulator.Tests/Scripts/NumberParsing.cs +++ b/YololEmulator.Tests/Scripts/NumberParsing.cs @@ -65,7 +65,10 @@ public Number Azurethi(Number value) [TestMethod] public void Azurethi() { - Azurethi(DateTime.UtcNow.Ticks); + var rng = new Random(); + var input = rng.Next(); + Console.WriteLine("Input:" + input); + Azurethi(input); } } } diff --git a/YololEmulator.Tests/Scripts/ParserTorture.cs b/YololEmulator.Tests/Scripts/ParserTorture.cs index 3212583..99f39db 100644 --- a/YololEmulator.Tests/Scripts/ParserTorture.cs +++ b/YololEmulator.Tests/Scripts/ParserTorture.cs @@ -1,4 +1,6 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yolol.Grammar; namespace YololEmulator.Tests.Scripts { @@ -8,7 +10,41 @@ public class ParserTorture [TestMethod] public void MethodName() { - var ast = TestExecutor.Parse(":d=1 goto (:a--)"); + var result = Parser.ParseProgram( + "goto TODO//comment\ngoto TODO goto TODO\n" + + "if TODO then goto TODO else goto TODO end\n" + + "if TODO then ww=TODO end\n" + + "x++ ++yy --z zz--\n" + + "c+=TODO c0-=TODO d0*=TODO d0/=TODO d0%=TODO d0^=TODO\n" + ); + + if (result.IsOk) + { + Console.WriteLine("```"); + Console.WriteLine(result.Ok.ToString()); + Console.WriteLine("```"); + } + else + Console.WriteLine(result.Err); + } + + [TestMethod] + public void Expression() + { + var result = Parser.ParseProgram(":a=:b+:c+:d goto ++:done"); + + if (result.IsOk) + { + Console.WriteLine("```"); + Console.WriteLine(result.Ok.ToString()); + Console.WriteLine("```"); + } + else + { + Console.WriteLine(result.Err); + + + } } } } diff --git a/YololEmulator.Tests/TestExecutor.cs b/YololEmulator.Tests/TestExecutor.cs index 5fc2a3b..5859049 100644 --- a/YololEmulator.Tests/TestExecutor.cs +++ b/YololEmulator.Tests/TestExecutor.cs @@ -11,13 +11,11 @@ public static class TestExecutor { public static Program Parse(params string[] lines) { - var tokens = Tokenizer.TryTokenize(string.Join("\n", lines)); - Assert.IsTrue(tokens.HasValue, tokens.FormatErrorMessageFragment()); + var result = Parser.ParseProgram(string.Join("\n", lines) + "\n"); + if (!result.IsOk) + Assert.Fail(result.Err.ToString()); - var parsed = Parser.TryParseProgram(tokens.Value); - Assert.IsTrue(parsed.HasValue, parsed.FormatErrorMessageFragment()); - - return parsed.Value; + return result.Ok; } @@ -33,6 +31,10 @@ public static MachineState Execute(Program p) public static MachineState Execute(IDeviceNetwork network, params string[] lines) { + var result = Parser.ParseProgram(string.Join("\n", lines) + "\n"); + if (!result.IsOk) + Assert.Fail(result.Err.ToString()); + var state = new MachineState(network); var pc = 0; @@ -41,14 +43,8 @@ public static MachineState Execute(IDeviceNetwork network, params string[] lines if (pc >= lines.Length) break; - var line = lines[pc]; - var tokens = Tokenizer.TryTokenize(line); - Assert.IsTrue(tokens.HasValue, tokens.FormatErrorMessageFragment()); - - var parsed = Parser.TryParseLine(tokens.Value); - Assert.IsTrue(parsed.HasValue, parsed.FormatErrorMessageFragment()); - - pc = parsed.Value.Evaluate(pc, state); + var line = result.Ok.Lines[pc]; + pc = line.Evaluate(pc, state); } return state; @@ -79,6 +75,10 @@ public static MachineState Execute2(int count, params string[] lines) public static MachineState Execute2(int count, IDeviceNetwork network, params string[] lines) { + var result = Parser.ParseProgram(string.Join("\n", lines)); + if (!result.IsOk) + Assert.Fail(result.Err.ToString()); + var state = new MachineState(network); var pc = 0; @@ -87,16 +87,10 @@ public static MachineState Execute2(int count, IDeviceNetwork network, params st if (pc >= lines.Length || count-- <= 0) break; - var line = lines[pc]; - var tokens = Tokenizer.TryTokenize(line); - Assert.IsTrue(tokens.HasValue, tokens.FormatErrorMessageFragment()); - - var parsed = Parser.TryParseLine(tokens.Value); - Assert.IsTrue(parsed.HasValue, parsed.FormatErrorMessageFragment()); - + var line = result.Ok.Lines[pc]; try { - pc = parsed.Value.Evaluate(pc, state); + pc = line.Evaluate(pc, state); } catch (ExecutionException) { diff --git a/YololEmulator/Program.cs b/YololEmulator/Program.cs index bba36f5..2540dd1 100644 --- a/YololEmulator/Program.cs +++ b/YololEmulator/Program.cs @@ -3,9 +3,7 @@ using System.IO; using System.Linq; using CommandLine; -using Superpower.Model; using Yolol.Execution; -using Yolol.Grammar; using YololEmulator.Network; using YololEmulator.Network.Http; using Parser = Yolol.Grammar.Parser; @@ -128,43 +126,11 @@ private static string ReadLine(string filepath, int lineNumber) private static int EvaluateLine(string line, int pc, MachineState state) { - var tokens = Tokenizer.TryTokenize(line); - if (!tokens.HasValue) + var result = Parser.ParseProgram(line); + if (!result.IsOk) { - Error(() => { - ErrorSpan(tokens.Location); - Console.WriteLine(); - - Console.Error.WriteLine(tokens.FormatErrorMessageFragment()); - }); - - Console.WriteLine("Press any key to try this line again"); - Console.ReadKey(true); - return pc; - } - - var parsed = Parser.TryParseLine(tokens.Value); - if (!parsed.HasValue) - { - Error(() => { - ErrorSpan(tokens.Location); - Console.WriteLine(); - - Console.Error.WriteLine(parsed.FormatErrorMessageFragment()); - }); - - Console.WriteLine("Press any key to try this line again"); - Console.ReadKey(true); - return pc; - } - - if (parsed.Remainder.Any()) - { - Error(() => { - Console.WriteLine("Failed to parse entire line"); - foreach (var token in parsed.Remainder) - Console.WriteLine($" - {token}"); - }); + Console.WriteLine(); + Console.Error.Write(result.Err.ToString()); Console.WriteLine("Press any key to try this line again"); Console.ReadKey(true); @@ -173,7 +139,7 @@ private static int EvaluateLine(string line, int pc, MachineState state) try { - return parsed.Value.Evaluate(pc, state); + return result.Ok.Lines[0].Evaluate(pc, state); } catch (ExecutionException ee) { @@ -185,14 +151,6 @@ private static int EvaluateLine(string line, int pc, MachineState state) } } - private static void ErrorSpan(TextSpan location) - { - for (var i = 0; i < location.Position.Column + 4; i++) - Console.Write('-'); - for (var i = 0; i < location.Length; i++) - Console.Error.Write('^'); - } - private static void Error(Action act) { var fg = Console.ForegroundColor; diff --git a/YololEmulator/YololEmulator.csproj b/YololEmulator/YololEmulator.csproj index e99f380..5df7560 100644 --- a/YololEmulator/YololEmulator.csproj +++ b/YololEmulator/YololEmulator.csproj @@ -37,7 +37,6 @@ - diff --git a/Yololc/Program.cs b/Yololc/Program.cs index bd9c10f..7db4ddf 100644 --- a/Yololc/Program.cs +++ b/Yololc/Program.cs @@ -7,7 +7,6 @@ using Newtonsoft.Json; using Yolol.Analysis; using Yolol.Analysis.TreeVisitor.Reduction; -using Yolol.Grammar; namespace Yololc { @@ -100,23 +99,12 @@ private static string ReadInput(Options options) { Yolol.Grammar.AST.Program? TryParseAsYolol(ICollection log) { - var tokens = Tokenizer.TryTokenize(input); - if (!tokens.HasValue) - { - log.Add($"{tokens.FormatErrorMessageFragment()}"); - log.Add(tokens.ErrorPosition.ToString()); - return null; - } - - var astResult = Yolol.Grammar.Parser.TryParseProgram(tokens.Value); - if (!astResult.HasValue) - { - log.Add($"{astResult.FormatErrorMessageFragment()}"); - log.Add(astResult.ErrorPosition.ToString()); - return null; - } + var result = Yolol.Grammar.Parser.ParseProgram(input); + if (result.IsOk) + return result.Ok; - return astResult.Value; + log.Add($"{result.Err.Message} @ {result.Err.Cursor}"); + return null; } Yolol.Grammar.AST.Program? TryParseAsAst(ICollection log)