private Yarn.Type CheckOperation(ParserRuleContext context, ParserRuleContext[] terms, string operationType, params Yarn.Type[] permittedTypes) { var termTypes = new List <Yarn.Type>(); var expressionType = Yarn.Type.Undefined; foreach (var expression in terms) { // Visit this expression, and determine its type. Yarn.Type type = Visit(expression); if (type != Yarn.Type.Undefined) { termTypes.Add(type); if (expressionType == Yarn.Type.Undefined) { // This is the first concrete type we've seen. This // will be our expression type. expressionType = type; } } } if (permittedTypes.Length == 1 && expressionType == Yarn.Type.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 == Yarn.Type.Undefined) { // We still don't know what type of expression this is, and // don't have a reasonable guess. throw new TypeException(context, $"Type of expression {context.GetText()} 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)", sourceFileName); } // 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. var variableNames = terms .OfType <YarnSpinnerParser.ExpressionContext>() .Select(c => c.GetChild <YarnSpinnerParser.ValueVarContext>(0)) .Where(c => c != null) .Select(v => v.variable().VAR_ID().GetText()) .Distinct(); // Build the list of variable names that we don't have a // declaration for. We'll check for explicit declarations first. var undefinedVariableNames = variableNames .Where(name => Declarations.Any(d => d.Name == name) == false); if (undefinedVariableNames.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 undefinedVariableName in undefinedVariableNames) { // Generate a declaration for this variable here. var decl = new Declaration { Name = undefinedVariableName, DeclarationType = Declaration.Type.Variable, Description = $"{System.IO.Path.GetFileName(sourceFileName)}, node {currentNodeName}, line {positionInFile - nodePositionInFile}", ReturnType = expressionType, DefaultValue = DefaultValueForType(expressionType), SourceFileName = sourceFileName, SourceFileLine = positionInFile, SourceNodeName = currentNodeName, SourceNodeLine = positionInFile - nodePositionInFile, 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); throw new TypeException(context, $"All terms of {operationType} must be the same, not {typeList}", sourceFileName); } // The expression type must match one of the permitted types. // If the type we've found is one of these permitted types, // return it - the expression is valid, and we're done. if (permittedTypes.Contains(expressionType)) { return(expressionType); } else { // The expression type wasn't valid! var permittedTypesList = string.Join(" or ", permittedTypes); var typeList = string.Join(", ", termTypes); throw new TypeException(context, $"Terms of '{operationType}' must be {permittedTypesList}, not {typeList}", sourceFileName); } }
public override Yarn.Type VisitValueFunc(YarnSpinnerParser.ValueFuncContext context) { string functionName = context.function().FUNC_ID().GetText(); Declaration functionDeclaration = Declarations .Where(d => d.DeclarationType == Declaration.Type.Function) .FirstOrDefault(d => d.Name == functionName); if (functionDeclaration == null) { // We don't have a declaration for this function. Create an // implicit one. functionDeclaration = new Declaration { Name = functionName, DeclarationType = Declaration.Type.Function, IsImplicit = true, ReturnType = Yarn.Type.Undefined, Description = $"Implicit declaration of function at {sourceFileName}:{context.Start.Line}:{context.Start.Column}", SourceFileName = sourceFileName, SourceFileLine = context.Start.Line, SourceNodeName = currentNodeName, SourceNodeLine = context.Start.Line - (this.currentNodeContext.BODY_START().Symbol.Line + 1), }; // Create the array of parameters for this function based // on how many we've seen in this call. Set them all to be // undefined; we'll bind their type shortly. functionDeclaration.Parameters = context.function().expression() .Select(e => new Declaration.Parameter { Type = Yarn.Type.Undefined }) .ToArray(); NewDeclarations.Add(functionDeclaration); } // Check each parameter of the function var suppliedParameters = context.function().expression(); Declaration.Parameter[] expectedParameters = functionDeclaration.Parameters; if (suppliedParameters.Length != expectedParameters.Length) { // Wrong number of parameters supplied var parameters = expectedParameters.Length == 1 ? "parameter" : "parameters"; throw new TypeException(context, $"Function {functionName} expects {expectedParameters.Length} {parameters}, but received {suppliedParameters.Length}", sourceFileName); } for (int i = 0; i < expectedParameters.Length; i++) { var suppliedParameter = suppliedParameters[i]; var expectedType = expectedParameters[i].Type; var suppliedType = this.Visit(suppliedParameter); if (expectedType == Yarn.Type.Undefined) { // The type of this parameter hasn't yet been bound. // Bind this parameter type to what we've resolved the // type to. expectedParameters[i].Type = suppliedType; expectedType = suppliedType; } if (suppliedType != expectedType) { throw new TypeException(context, $"{functionName} parameter {i + 1} expects a {expectedType}, not a {suppliedType}", sourceFileName); } } // Cool, all the parameters check out! // Finally, return the return type of this function. return(functionDeclaration.ReturnType); }
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); } } // 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) { // 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 = DefaultValueForType(expressionType), 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); } } }
public override Yarn.IType VisitSet_statement([NotNull] YarnSpinnerParser.Set_statementContext context) { var variableContext = context.variable(); var expressionContext = context.expression(); if (expressionContext == null || variableContext == null) { return(BuiltinTypes.Undefined); } var expressionType = base.Visit(expressionContext); var variableType = base.Visit(variableContext); var variableName = variableContext.GetText(); ParserRuleContext[] terms = { variableContext, expressionContext }; Yarn.IType type; Operator @operator; switch (context.op.Type) { case YarnSpinnerLexer.OPERATOR_ASSIGNMENT: // Straight assignment supports any assignment, as long // as it's consistent; we already know the type of the // expression, so let's check to see if it's assignable // to the type of the variable if (variableType != BuiltinTypes.Undefined && TypeUtil.IsSubType(variableType, expressionType) == false) { string message = $"{variableName} ({variableType?.Name ?? "undefined"}) cannot be assigned a {expressionType?.Name ?? "undefined"}"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, message)); } else if (variableType == BuiltinTypes.Undefined && expressionType != BuiltinTypes.Undefined) { // This variable was undefined, but we have a // defined type for the value it was set to. Create // an implicit declaration for the variable! // The start line of the body is the line after the delimiter int nodePositionInFile = this.currentNodeContext.BODY_START().Symbol.Line + 1; // Generate a declaration for this variable here. var decl = new Declaration { Name = variableName, Description = $"Implicitly declared in {System.IO.Path.GetFileName(sourceFileName)}, node {currentNodeName}", Type = expressionType, DefaultValue = DefaultValueForType(expressionType), SourceFileName = sourceFileName, SourceNodeName = currentNodeName, Range = new Range { Start = { Line = variableContext.Start.Line - 1, Character = variableContext.Start.Column, }, End = { Line = variableContext.Stop.Line - 1, Character = variableContext.Stop.Column + variableContext.GetText().Length, }, }, IsImplicit = true, }; NewDeclarations.Add(decl); } break; case YarnSpinnerLexer.OPERATOR_MATHS_ADDITION_EQUALS: // += supports strings and numbers @operator = CodeGenerationVisitor.TokensToOperators[YarnSpinnerLexer.OPERATOR_MATHS_ADDITION]; type = CheckOperation(context, terms, @operator, context.op.Text); break; case YarnSpinnerLexer.OPERATOR_MATHS_SUBTRACTION_EQUALS: // -=, *=, /=, %= supports only numbers @operator = CodeGenerationVisitor.TokensToOperators[YarnSpinnerLexer.OPERATOR_MATHS_SUBTRACTION]; type = CheckOperation(context, terms, @operator, context.op.Text); break; case YarnSpinnerLexer.OPERATOR_MATHS_MULTIPLICATION_EQUALS: @operator = CodeGenerationVisitor.TokensToOperators[YarnSpinnerLexer.OPERATOR_MATHS_MULTIPLICATION]; type = CheckOperation(context, terms, @operator, context.op.Text); break; case YarnSpinnerLexer.OPERATOR_MATHS_DIVISION_EQUALS: @operator = CodeGenerationVisitor.TokensToOperators[YarnSpinnerLexer.OPERATOR_MATHS_DIVISION]; type = CheckOperation(context, terms, @operator, context.op.Text); break; case YarnSpinnerLexer.OPERATOR_MATHS_MODULUS_EQUALS: @operator = CodeGenerationVisitor.TokensToOperators[YarnSpinnerLexer.OPERATOR_MATHS_MODULUS]; type = CheckOperation(context, terms, @operator, context.op.Text); break; default: throw new InvalidOperationException($"Internal error: {nameof(VisitSet_statement)} got unexpected operand {context.op.Text}"); } if (expressionType == BuiltinTypes.Undefined) { // We don't know what this is set to, so we'll have to // assume it's ok. Return the variable type, if known. return(variableType); } return(expressionType); }
public override Yarn.IType VisitValueFunc(YarnSpinnerParser.ValueFuncContext context) { string functionName = context.function_call().FUNC_ID().GetText(); Declaration functionDeclaration = Declarations .Where(d => d.Type is FunctionType) .FirstOrDefault(d => d.Name == functionName); FunctionType functionType; if (functionDeclaration == null) { // We don't have a declaration for this function. Create an // implicit one. functionType = new FunctionType(); functionType.ReturnType = BuiltinTypes.Undefined; functionDeclaration = new Declaration { Name = functionName, Type = functionType, IsImplicit = true, Description = $"Implicit declaration of function at {sourceFileName}:{context.Start.Line}:{context.Start.Column}", SourceFileName = sourceFileName, SourceNodeName = currentNodeName, Range = new Range { Start = { Line = context.Start.Line - 1, Character = context.Start.Column, }, End = { Line = context.Stop.Line - 1, Character = context.Stop.Column + context.Stop.Text.Length, }, }, }; // Create the array of parameters for this function based // on how many we've seen in this call. Set them all to be // undefined; we'll bind their type shortly. var parameterTypes = context.function_call().expression() .Select(e => BuiltinTypes.Undefined) .ToList(); foreach (var parameterType in parameterTypes) { functionType.AddParameter(parameterType); } NewDeclarations.Add(functionDeclaration); } else { functionType = functionDeclaration.Type as FunctionType; if (functionType == null) { throw new InvalidOperationException($"Internal error: decl's type is not a {nameof(FunctionType)}"); } } // Check each parameter of the function var suppliedParameters = context.function_call().expression(); var expectedParameters = functionType.Parameters; if (suppliedParameters.Length != expectedParameters.Count()) { // Wrong number of parameters supplied var parameters = expectedParameters.Count() == 1 ? "parameter" : "parameters"; this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, $"Function {functionName} expects {expectedParameters.Count()} {parameters}, but received {suppliedParameters.Length}")); return(functionType.ReturnType); } for (int i = 0; i < expectedParameters.Count(); i++) { var suppliedParameter = suppliedParameters[i]; var expectedType = expectedParameters[i]; var suppliedType = this.Visit(suppliedParameter); if (expectedType == BuiltinTypes.Undefined) { // The type of this parameter hasn't yet been bound. // Bind this parameter type to what we've resolved the // type to. expectedParameters[i] = suppliedType; expectedType = suppliedType; } if (TypeUtil.IsSubType(expectedType, suppliedType) == false) { this.diagnostics.Add(new Diagnostic(this.sourceFileName, context, $"{functionName} parameter {i + 1} expects a {expectedType?.Name ?? "undefined"}, not a {suppliedType?.Name ?? "undefined"}")); return(functionType.ReturnType); } } // Cool, all the parameters check out! // Finally, return the return type of this function. return(functionType.ReturnType); }
public override Yarn.Type VisitDeclare_statement(YarnSpinnerParser.Declare_statementContext context) { // Get the name of the variable we're declaring string variableName = context.variable().GetText(); // Does this variable name already exist in our declarations? var existingExplicitDeclaration = Declarations.Where(d => d.IsImplicit == false).FirstOrDefault(d => d.Name == variableName); if (existingExplicitDeclaration != null) { // Then this is an error, because you can't have two explicit declarations for the same variable. throw new TypeException(context, $"{existingExplicitDeclaration.Name} has already been declared in {existingExplicitDeclaration.SourceFileName}, line {existingExplicitDeclaration.SourceFileLine}", sourceFileName); } // Figure out the value and its type var constantValueVisitor = new ConstantValueVisitor(sourceFileName); var value = constantValueVisitor.Visit(context.value()); // Do we have an explicit type declaration? if (context.type() != null) { Yarn.Type explicitType; // Get its type switch (context.type().typename.Type) { case YarnSpinnerLexer.TYPE_STRING: explicitType = Yarn.Type.String; break; case YarnSpinnerLexer.TYPE_BOOL: explicitType = Yarn.Type.Bool; break; case YarnSpinnerLexer.TYPE_NUMBER: explicitType = Yarn.Type.Number; break; default: throw new ParseException(context, $"Unknown type {context.type().GetText()}"); } // Check that it matches - if it doesn't, that's a type // error if (explicitType != value.type) { throw new TypeException(context, $"Type {context.type().GetText()} does not match value {context.value().GetText()} ({value.type})", sourceFileName); } } // Get the variable declaration, if we have one string description = null; if (context.Description != null) { description = context.Description.Text.Trim('"'); } // We're done creating the declaration! 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; var declaration = new Declaration { Name = variableName, ReturnType = value.type, DefaultValue = value.value, Description = description, DeclarationType = Declaration.Type.Variable, SourceFileName = sourceFileName, SourceFileLine = positionInFile, SourceNodeName = currentNodeName, SourceNodeLine = positionInFile - nodePositionInFile, IsImplicit = false, }; this.NewDeclarations.Add(declaration); return(value.type); }