From ef172055d87533bf391dc4b272578d24a7a961e7 Mon Sep 17 00:00:00 2001 From: Jon Manning Date: Mon, 9 Oct 2023 18:42:37 +1100 Subject: [PATCH] Add type-check requirements for arithmetic --- ...tors-AdditionsRequireNumbersOrStrings.yarn | 4 + YarnSpinner.Compiler/TypeCheckerListener.cs | 82 ++++++++++++++++--- 2 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 Tests/TestCases/ParseFailures/Operators-AdditionsRequireNumbersOrStrings.yarn diff --git a/Tests/TestCases/ParseFailures/Operators-AdditionsRequireNumbersOrStrings.yarn b/Tests/TestCases/ParseFailures/Operators-AdditionsRequireNumbersOrStrings.yarn new file mode 100644 index 000000000..ad915be9e --- /dev/null +++ b/Tests/TestCases/ParseFailures/Operators-AdditionsRequireNumbersOrStrings.yarn @@ -0,0 +1,4 @@ +title: Start +--- +<> // error '+' cannot be used with a bool +=== \ No newline at end of file diff --git a/YarnSpinner.Compiler/TypeCheckerListener.cs b/YarnSpinner.Compiler/TypeCheckerListener.cs index 1ecd3429e..ed28269c5 100644 --- a/YarnSpinner.Compiler/TypeCheckerListener.cs +++ b/YarnSpinner.Compiler/TypeCheckerListener.cs @@ -140,6 +140,10 @@ internal class TypeCheckerListener : YarnSpinnerParserBaseListener /// be added to while walking the parse tree. /// An existing type solution to build /// upon. + /// A collection of objects. During type checking, this + /// collection will be added to if a type constraint fails to resolve by + /// the end of a file. public TypeCheckerListener(string sourceFileName, CommonTokenStream tokens, List knownDeclarations, List knownTypes, Substitution typeSolution, HashSet failingTypeConstraints) { this.sourceFileName = sourceFileName; @@ -168,19 +172,58 @@ private void AddDiagnostic(ParserRuleContext context, string message, Diagnostic private TypeEqualityConstraint AddEqualityConstraint(IType a, IType b, ParserRuleContext context, FailureMessageProvider failureMessageProvider) { - TypeEqualityConstraint item = new TypeEqualityConstraint(a ?? Types.Error, b ?? Types.Error); - item.SourceFileName = this.sourceFileName; - item.SourceRange = GetRange(context); - item.FailureMessageProvider = failureMessageProvider; - item.SourceExpression = context.GetTextWithWhitespace(); + TypeEqualityConstraint item = new TypeEqualityConstraint(a, b) + { + SourceFileName = this.sourceFileName, + SourceRange = GetRange(context), + FailureMessageProvider = failureMessageProvider, + SourceExpression = context.GetTextWithWhitespace() + }; this.TypeEquations.Add(item); return item; } + private TypeConstraint AddEqualityDisjunctionConstraint(IType a, IEnumerable b, ParserRuleContext context, FailureMessageProvider failureMessageProvider) + { + if (b.Count() == 0) + { + // We didn't get any types that 'a' must be equal to, and that's + // a problem. Produce a constraint saying that 'a' is an error. + return AddEqualityConstraint(a, Types.Error, context, failureMessageProvider); + } else if (b.Count() == 1) { + // We got a single type to constrain against. Don't bother + // creating a disjunction with only a single constraint - just + // the constraint will do. + return AddEqualityConstraint(a, b.Single(), context, failureMessageProvider); + } + + var disjunction = new DisjunctionConstraint( + b.Select(other => + { + return new TypeEqualityConstraint(a, other) + { + SourceExpression = context.GetTextWithWhitespace(), + SourceFileName = this.sourceFileName, + SourceRange = GetRange(context), + FailureMessageProvider = failureMessageProvider, + }; + }) + ) + { + SourceExpression = context.GetTextWithWhitespace(), + SourceFileName = this.sourceFileName, + SourceRange = GetRange(context), + FailureMessageProvider = failureMessageProvider, + }; + + this.TypeEquations.Add(disjunction); + return disjunction; + } + private TypeConvertibleConstraint AddConvertibleConstraint(IType from, IType to, ParserRuleContext context, FailureMessageProvider failureMessageProvider) { - TypeConvertibleConstraint item = new TypeConvertibleConstraint(from ?? Types.Error, to ?? Types.Error); + TypeConvertibleConstraint item = new TypeConvertibleConstraint(from, to); item.SourceFileName = this.sourceFileName; item.SourceRange = GetRange(context); item.FailureMessageProvider = failureMessageProvider; @@ -447,8 +490,23 @@ public override void ExitExpAddSub([NotNull] YarnSpinnerParser.ExpAddSubContext IType operandAType = context.expression(0)?.Type ?? Types.Error; IType operandBType = context.expression(1)?.Type ?? Types.Error; + IEnumerable permittedTypes; + string op = context.op.Text; + switch (op) { + case "+": + permittedTypes = new[] { Types.Number, Types.String }; + break; + case "-": + permittedTypes = new[] { Types.Number }; + break; + default: + throw new InvalidOperationException($"Internal error: {typeof(YarnSpinnerParser.ExpAddSubContext)} had invalid op \"{op}\""); + } + + this.AddEqualityDisjunctionConstraint(type, permittedTypes, context, s => $"Operation '{op}' can't be used with a value of type {operandAType.Substitute(s)}"); + this.AddEqualityConstraint(type, operandAType, context, s => $"Operation '{op}' can't be used with a value of type {operandAType.Substitute(s)}"); this.AddEqualityConstraint(operandAType, operandBType, context, s => $"Operation '{op}'s values must both be the same type, not {operandAType.Substitute(s)} and {operandBType.Substitute(s)}"); } @@ -463,6 +521,8 @@ public override void ExitExpMultDivMod([NotNull] YarnSpinnerParser.ExpMultDivMod string op = context.op.Text; + this.AddEqualityConstraint(type, Types.Number, context, s => $"Operation '{op}' can only be used with {Types.Number}"); + this.AddEqualityConstraint(type, operandAType, context, s => $"Operation '{op}' can't be used with a value of type {operandAType.Substitute(s)}"); this.AddEqualityConstraint(operandAType, operandBType, context, s => $"Operation '{op}'s values must both be the same type, not {operandAType.Substitute(s)} and {operandBType.Substitute(s)}"); } @@ -499,18 +559,16 @@ public override void ExitExpAndOrXor([NotNull] YarnSpinnerParser.ExpAndOrXorCont { // The result of a logical and, or, or xor is boolean; the types of // the expressions must also be boolean. - var exp0Type = context.expression(0)?.Type; - var exp1Type = context.expression(1)?.Type; - if (exp0Type == null || exp1Type == null) + var type0 = context.expression(0)?.Type; + var type1 = context.expression(1)?.Type; + if (type0 == null || type1 == null) { context.Type = Types.Error; return; } context.Type = Types.Boolean; - IType type0 = context.expression(0).Type; - IType type1 = context.expression(1).Type; - + this.AddEqualityConstraint(type0, Types.Boolean, context, s => $"{context.op.Text} operands must be {Types.Boolean}, not {type0.Substitute(s)}"); this.AddEqualityConstraint(type0, type1, context, s => $"{context.op.Text} operands must be the same type, not {type0} and {type1}"); }