/// <summary> /// Initializes a new instance of the <see cref="Diagnostic"/> class. /// </summary> /// <param name="fileName"><inheritdoc cref="FileName" /// path="/summary/node()"/></param> /// <param name="context">The parse node at which the error /// occurred.</param> /// <param name="message"><inheritdoc cref="Message" /// path="/summary/node()"/></param> /// <param name="severity"><inheritdoc cref="Severity" /// path="/summary/node()"/></param> public Diagnostic(string fileName, ParserRuleContext context, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) { this.FileName = fileName; if (context != null) { this.Range = new Range( context.Start.Line - 1, context.Start.Column, context.Stop.Line - 1, context.Stop.Column + context.Stop.Text.Length); } this.Message = message; this.Context = context.GetTextWithWhitespace(); this.Severity = severity; }
// ok so what do we actually need to do in here? // we need to do a few different things // basically we need to go through the various types in the expression // if any are known we need to basically log that // then at the end if there are still unknowns we check if the operation itself forces a type // so if we have say Undefined = Undefined + Number then we know that only one operation supports + Number and that is Number + Number // so we can slot the type into the various parts private Yarn.IType CheckOperation(ParserRuleContext context, ParserRuleContext[] terms, Operator operationType, string operationDescription, params Yarn.IType[] permittedTypes) { var termTypes = new List <Yarn.IType>(); var expressionType = BuiltinTypes.Undefined; foreach (var expression in terms) { // Visit this expression, and determine its type. Yarn.IType type = Visit(expression); if (type != BuiltinTypes.Undefined) { termTypes.Add(type); if (expressionType == BuiltinTypes.Undefined) { // This is the first concrete type we've seen. This // will be our expression type. expressionType = type; } } } if (permittedTypes.Length == 1 && expressionType == BuiltinTypes.Undefined) { // If we aren't sure of the expression type from // parameters, but we only have one permitted one, then // assume that the expression type is the single permitted // type. expressionType = permittedTypes.First(); } if (expressionType == BuiltinTypes.Undefined) { // We still don't know what type of expression this is, and // don't have a reasonable guess. // Last-ditch effort: is the operator that we were given // valid in exactly one type? In that case, we'll decide // it's that type. var typesImplementingMethod = types .Where(t => t.Methods != null) .Where(t => t.Methods.ContainsKey(operationType.ToString())); if (typesImplementingMethod.Count() == 1) { // Only one type implements the operation we were // given. Given no other information, we will assume // that it is this type. expressionType = typesImplementingMethod.First(); } else if (typesImplementingMethod.Count() > 1) { // Multiple types implement this operation. IEnumerable <string> typeNames = typesImplementingMethod.Select(t => t.Name); string message = $"Type of expression \"{context.GetTextWithWhitespace()}\" can't be determined without more context (the compiler thinks it could be {string.Join(", or ", typeNames)}). Use a type cast on at least one of the terms (e.g. the string(), number(), bool() functions)"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); return(BuiltinTypes.Undefined); } else { // No types implement this operation (??) string message = $"Type of expression \"{context.GetTextWithWhitespace()}\" can't be determined without more context. Use a type cast on at least one of the terms (e.g. the string(), number(), bool() functions)"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); return(BuiltinTypes.Undefined); } } // to reach this point we have either worked out the final type of the expression // or had to give up, and if we gave up we have nothing left to do // there are then two parts to this, first we need to declare the implict type of any variables (that appears to be working) // or the implicit type of any function. // annoyingly the function will already have an implicit definition created for it // we will have to strip that out and add in a new one with the new return type foreach (var term in terms) { if (term is YarnSpinnerParser.ExpValueContext) { var value = ((YarnSpinnerParser.ExpValueContext)term).value(); if (value is YarnSpinnerParser.ValueFuncContext) { var id = ((YarnSpinnerParser.ValueFuncContext)value).function_call().FUNC_ID().GetText(); Declaration functionDeclaration = NewDeclarations.Where(d => d.Type is FunctionType).FirstOrDefault(d => d.Name == id); if (functionDeclaration != null) { var func = functionDeclaration.Type as FunctionType; if (func?.ReturnType == BuiltinTypes.Undefined) { NewDeclarations.Remove(functionDeclaration); func.ReturnType = expressionType; NewDeclarations.Add(functionDeclaration); } } else { Visit(term); } } } } // Were any of the terms variables for which we don't currently // have a declaration for? // Start by building a list of all terms that are variables. // These are either variable values, or variable names . (The // difference between these two is that a ValueVarContext // occurs in syntax where the value of the variable is used // (like an expression), while a VariableContext occurs in // syntax where it's just a variable name (like a set // statements) // All VariableContexts in the terms of this expression (but // not in the children of those terms) var variableContexts = terms .Select(c => c.GetChild <YarnSpinnerParser.ValueVarContext>(0)?.variable()) .Concat(terms.Select(c => c.GetChild <YarnSpinnerParser.VariableContext>(0))) .Concat(terms.OfType <YarnSpinnerParser.VariableContext>()) .Concat(terms.OfType <YarnSpinnerParser.ValueVarContext>().Select(v => v.variable())) .Where(c => c != null); // Build the list of variable contexts that we don't have a // declaration for. We'll check for explicit declarations first. var undefinedVariableContexts = variableContexts .Where(v => Declarations.Any(d => d.Name == v.VAR_ID().GetText()) == false) .Distinct(); if (undefinedVariableContexts.Count() > 0) { // We have references to variables that we don't have a an // explicit declaration for! Time to create implicit // references for them! // Get the position of this reference in the file int positionInFile = context.Start.Line; // The start line of the body is the line after the delimiter int nodePositionInFile = this.currentNodeContext.BODY_START().Symbol.Line + 1; foreach (var undefinedVariableContext in undefinedVariableContexts) { // We can only create an implicit declaration for a variable // if we have a default value for it, because all variables // are required to have a value. If we can't, it's generally // because we couldn't figure out a concrete type for the // variable given the context. var canGetDefaultValue = TryGetDefaultValueForType(expressionType, out var defaultValue); // If we can't produce this, then we can't generate the // declaration. if (!canGetDefaultValue) { this.diagnostics.Add(new Diagnostic(sourceFileName, undefinedVariableContext, string.Format(CantDetermineVariableTypeError, undefinedVariableContext.VAR_ID().GetText()))); continue; } // Generate a declaration for this variable here. var decl = new Declaration { Name = undefinedVariableContext.VAR_ID().GetText(), Description = $"Implicitly declared in {System.IO.Path.GetFileName(sourceFileName)}, node {currentNodeName}", Type = expressionType, DefaultValue = defaultValue, SourceFileName = sourceFileName, SourceNodeName = currentNodeName, Range = new Range { Start = { Line = undefinedVariableContext.Start.Line - 1, Character = undefinedVariableContext.Start.Column, }, End = { Line = undefinedVariableContext.Stop.Line - 1, Character = undefinedVariableContext.Stop.Column + undefinedVariableContext.Stop.Text.Length, }, }, IsImplicit = true, }; NewDeclarations.Add(decl); } } // All types must be same as the expression type (which is the // first defined type we encountered when going through the // terms) if (termTypes.All(t => t == expressionType) == false) { // Not all the term types we found were the expression // type. var typeList = string.Join(", ", termTypes.Select(t => t.Name)); string message = $"All terms of {operationDescription} must be the same, not {typeList}"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); return(BuiltinTypes.Undefined); } // We've now determined that this expression is of // expressionType. In case any of the terms had an undefined // type, we'll define it now. foreach (var term in terms) { if (term is YarnSpinnerParser.ExpressionContext expression) { if (expression.Type == BuiltinTypes.Undefined) { expression.Type = expressionType; } if (expression.Type is FunctionType functionType && functionType.ReturnType == BuiltinTypes.Undefined) { functionType.ReturnType = expressionType; } } } if (operationType != Operator.None) { // We need to validate that the type we've selected actually // implements this operation. var implementingType = TypeUtil.FindImplementingTypeForMethod(expressionType, operationType.ToString()); if (implementingType == null) { string message = $"{expressionType.Name} has no implementation defined for {operationDescription}"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); return(BuiltinTypes.Undefined); } } // Is this expression is required to be one of the specified types? if (permittedTypes.Count() > 0) { // Is the type that we've arrived at compatible with one of // the permitted types? if (permittedTypes.Any(t => TypeUtil.IsSubType(t, expressionType))) { // It's compatible! Great, return the type we've // determined. return(expressionType); } else { // The expression type wasn't valid! var permittedTypesList = string.Join(" or ", permittedTypes.Select(t => t?.Name ?? "undefined")); var typeList = string.Join(", ", termTypes.Select(t => t.Name)); string message = $"Terms of '{operationDescription}' must be {permittedTypesList}, not {typeList}"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); return(BuiltinTypes.Undefined); } } else { // We weren't given a specific type. The expression type is // therefore only valid if it can use the provided // operator. // Find a type in 'expressionType's hierarchy that // implements this method. var implementingTypeForMethod = TypeUtil.FindImplementingTypeForMethod(expressionType, operationType.ToString()); if (implementingTypeForMethod == null) { // The type doesn't have a method for handling this // operator, and neither do any of its supertypes. This // expression is therefore invalid. string message = $"Operator {operationDescription} cannot be used with {expressionType.Name} values"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); return(BuiltinTypes.Undefined); } else { return(expressionType); } } }