diff options
Diffstat (limited to 'src/inc/CrstTypeTool.cs')
-rw-r--r-- | src/inc/CrstTypeTool.cs | 990 |
1 files changed, 990 insertions, 0 deletions
diff --git a/src/inc/CrstTypeTool.cs b/src/inc/CrstTypeTool.cs new file mode 100644 index 0000000000..d6d90e427b --- /dev/null +++ b/src/inc/CrstTypeTool.cs @@ -0,0 +1,990 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// This tool exists to transform a high level description of Crst dependencies (i.e. which Crst type may be +// acquired before or after other Crst types) into a header file that defines a enum to describe each Crst +// type and tables that map type to numerical ranking and a string based name. +// +// To use the tool, run "csc.exe CrstTypeTool.cs" and run the resulting executable. +// +// The Crst type definition file is written in a very simple language. Comments begin with '//' and continue +// to the end of the line. All remaining tokens after comment removal are simply sequences of non-whitespace +// characters separated by whitespace. Keywords are case-insensitive and identifiers (which are always Crst +// type names) are case sensitive. The language grammar is given below in EBNF-like form: +// +// TopLevel ::= CrstDefinition* +// +// CrstDefinition ::= 'Crst' <Crst type name> CrstDependency* 'End' +// +// CrstDependency ::= 'AcquiredBefore' <Crst type name>* +// | 'AcquiredAfter' <Crst type name>* +// | 'SameLevelAs' <Crst type name>* +// | 'Unordered' +// +// Crst type names match the CrstType enums used in the source code minus the 'Crst' prefix. For example +// CrstAppDomainCache is written as 'AppDomainCache' in the .def file. +// +// The dependency "A 'AcquiredBefore' B" indicates that CrstA may be legally held while CrstB is acquired. +// Similarly "A 'AcquiredAfter' B" indicates that CrstA may be legally acquired while CrstB is held. "A +// 'AcquiredBefore' B" is logically equivalent to "B 'AcquiredAfter' A" and authors may enter the dependency +// is whichever seems to make the most sense to them (or add both rules if they so desire). +// +// 'Unordered' indicates that the Crst type does not participate in ranking (there should be very few Crsts +// like this and those that are know how to avoid or deal with deadlocks manually). +// +// 'SameLevelAs' indicates the given Crst type may be acquired alongside any number of instances of the Crst +// types indicated. "A 'SameLevel' B" automatically implies "B 'SameLevel' A" so it's not necessary to specify +// the dependency both ways though authors can do so if they wish. +// +// Simple validation of the .def file (over and above syntax checking) is performed by this tool prior to +// emitting the header file. This will catch logic errors such as referencing a Crst type that is not +// defined or using the 'Unordered' attribute along with any other attribute within a single definition. It +// will also catch cycles in the dependency graph (i.e. definitions that logically describe a system where the +// Crst types can't be ranked). +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Text.RegularExpressions; + +// The main application class containing the program entry point. +class CrstTypeTool +{ + // A hash containing every Crst type defined by the input .def file along with its attributes. Keyed by + // Crst type name (which is case sensitive and doesn't include the 'Crst' enum prefix). + Dictionary<string, CrstType> m_crsts = new Dictionary<string, CrstType>(); + + // The program entry point. + public static int Main() + { + try + { + // Calculate the filenames of the input and output files. + string inputFile = "CrstTypes.def"; + string outputFile = "CrstTypes.h"; + + // A common error is to forget to check out the CrstTypes.h file first. Handle this case specially + // so we can give a good error message. + if (File.Exists(outputFile) && (File.GetAttributes(outputFile) & FileAttributes.ReadOnly) != 0) + { + Console.WriteLine(outputFile + " is read-only, you must check it out of TFS/SD first"); + return 2; + } + + // Create an instance of our application class to store state in (specifically the collection of + // Crst type definitions). + CrstTypeTool app = new CrstTypeTool(); + + // Create a parser for the CrstTypes.def file and run it over the input file (errors are signalled + // via exception, in common with all the following steps except validation). + new TypeFileParser().ParseFile(inputFile, app.m_crsts); + + // Validate the collection of Crst type definitions we built up during parsing for common logic + // errors and the presence of dependency cycles. False is returned from ValidateCrsts if an error + // was detected (an error message will have already been output to the console at this point). + if (!app.ValidateCrsts()) + return 3; + + // Perform a topological sort to map each Crst type to a numeric ranking. + app.LevelCrsts(); + + // Emit the new header file containing Crst type definitions and ranking information. + app.WriteHeaderFile(outputFile); + + // If we get here the transformation was successful; inform the user and we're done. + Console.WriteLine(outputFile + " successfully updated"); + return 0; + } + catch (TypeFileParser.ParseError pe) + { + // Syntax errors specific to parsing the input file. + Console.WriteLine("ParseError: " + pe.Message); + return 4; + } + catch (Exception e) + { + // Any other general errors (file I/O problems, out of memory etc.). + Console.WriteLine("Unexpected exception:"); + Console.WriteLine(e); + return 5; + } + } + + // Emit the CrstTypes.h output file. + void WriteHeaderFile(string fileName) + { + FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None); + StreamWriter writer = new StreamWriter(stream); + + // Create a collection based on all the Crst types we've stored in the hash. We do this so we can sort + // the Crst types we emit (lexically, based on type name). + Dictionary<string, CrstType>.ValueCollection crstCollection = m_crsts.Values; + CrstType[] crsts = new CrstType[crstCollection.Count]; + crstCollection.CopyTo(crsts, 0); + Array.Sort(crsts); + + // Emit the header. Contains copyright information, the usual goop to avoid multiple inclusion and a + // header comment to discourage direct editing and point the user at the CrstTypes.def file instead + // (where all will be explained in greater detail). + writer.WriteLine("//"); + writer.WriteLine("// Licensed to the .NET Foundation under one or more agreements."); + writer.WriteLine("// The .NET Foundation licenses this file to you under the MIT license."); + writer.WriteLine("// See the LICENSE file in the project root for more information."); + writer.WriteLine("//"); + writer.WriteLine(); + writer.WriteLine("#ifndef __CRST_TYPES_INCLUDED"); + writer.WriteLine("#define __CRST_TYPES_INCLUDED"); + writer.WriteLine(); + writer.WriteLine("// **** THIS IS AN AUTOMATICALLY GENERATED HEADER FILE -- DO NOT EDIT!!! ****"); + writer.WriteLine(); + writer.WriteLine("// This file describes the range of Crst types available and their mapping to a numeric level (used by the"); + writer.WriteLine("// runtime in debug mode to validate we're deadlock free). To modify these settings edit the"); + writer.WriteLine("// file:CrstTypes.def file and run the clr\\bin\\CrstTypeTool utility to generate a new version of this file."); + writer.WriteLine(); + + // Emit the CrstType enum to define a value for each crst type (along with the kNumberOfCrstTypes + // constant). + writer.WriteLine("// Each Crst type is declared as a value in the following CrstType enum."); + writer.WriteLine("enum CrstType"); + writer.WriteLine("{"); + for (int i = 0; i < crsts.Length; i++) + writer.WriteLine(" Crst" + crsts[i].Name + " = " + i.ToString() + ","); + writer.WriteLine(" kNumberOfCrstTypes = " + crsts.Length.ToString()); + writer.WriteLine("};"); + writer.WriteLine(); + + // This is the end of the regular part of the header included by most files. + writer.WriteLine("#endif // __CRST_TYPES_INCLUDED"); + writer.WriteLine(); + + // There is a second section of the header intended for inclusion only by vm\Crst.cpp. This contains + // some data tables used to map crst type to rank or name. We could instead define two separate + // headers, but on the whole it seems simpler to do it this way. + writer.WriteLine("// Define some debug data in one module only -- vm\\crst.cpp."); + writer.WriteLine("#if defined(__IN_CRST_CPP) && defined(_DEBUG)"); + writer.WriteLine(); + + // Emit the crst type to rank mapping table. + writer.WriteLine("// An array mapping CrstType to level."); + writer.WriteLine("int g_rgCrstLevelMap[] ="); + writer.WriteLine("{"); + foreach (CrstType crst in crsts) + writer.WriteLine(" " + crst.Level + ",\t\t\t// Crst" + crst.Name); + writer.WriteLine("};"); + writer.WriteLine(); + + // Emit the crst type to name mapping table. + writer.WriteLine("// An array mapping CrstType to a stringized name."); + writer.WriteLine("LPCSTR g_rgCrstNameMap[] ="); + writer.WriteLine("{"); + foreach (CrstType crst in crsts) + writer.WriteLine(" \"Crst" + crst.Name + "\","); + writer.WriteLine("};"); + writer.WriteLine(); + + // Emit the constant Crst.cpp uses to record an unordered rank. + writer.WriteLine("// Define a special level constant for unordered locks."); + writer.WriteLine("#define CRSTUNORDERED (-1)"); + writer.WriteLine(); + + // Emit a couple of inline helpers to map type to rank or name (and validate the type while they're at + // it). + writer.WriteLine("// Define inline helpers to map Crst types to names and levels."); + writer.WriteLine("inline static int GetCrstLevel(CrstType crstType)"); + writer.WriteLine("{"); + writer.WriteLine(" LIMITED_METHOD_CONTRACT;"); + writer.WriteLine(" _ASSERTE(crstType >= 0 && crstType < kNumberOfCrstTypes);"); + writer.WriteLine(" return g_rgCrstLevelMap[crstType];"); + writer.WriteLine("}"); + writer.WriteLine("inline static LPCSTR GetCrstName(CrstType crstType)"); + writer.WriteLine("{"); + writer.WriteLine(" LIMITED_METHOD_CONTRACT;"); + writer.WriteLine(" _ASSERTE(crstType >= 0 && crstType < kNumberOfCrstTypes);"); + writer.WriteLine(" return g_rgCrstNameMap[crstType];"); + writer.WriteLine("}"); + writer.WriteLine(); + + // And that's the end of the second section of the header file. + writer.WriteLine("#endif // defined(__IN_CRST_CPP) && defined(_DEBUG)"); + + writer.Close(); + stream.Close(); + } + + // Peform checking of the Crst type definitions we've read just read. Various forms of logic error are + // scanned for including cycles in the dependency graph. Returns true if no errors are found. If false is + // returned a descriptive error message will have already been written to the console. + bool ValidateCrsts() + { + // Look at each Crst type definition in turn. + foreach (CrstType crst in m_crsts.Values) + { + // Catch Crst types that are referenced but never defined. + if (!crst.Defined) + { + Console.WriteLine(String.Format("Error: CrstType 'Crst{0}' is referenced without being defined", + crst.Name)); + return false; + } + + // Catch the use of the 'Unordered' attribute alongside the 'AcquiredBefore' attribute (which + // indicates an ordering). + if (crst.Level == CrstType.CrstUnordered && (crst.AcquiredBeforeList.Count > 0 || + crst.Group != null)) + { + Console.WriteLine(String.Format("Error: CrstType 'Crst{0}' is declared as both unordered and acquired before 'Crst{1}'", + crst.Name, crst.AcquiredBeforeList[0].Name)); + return false; + } + + // Catch the use of the 'Unordered' attribute alongside the 'SameLevelAs' attribute (which + // indicates an ordering). + if (crst.Level == CrstType.CrstUnordered && crst.Group != null) + { + Console.WriteLine(String.Format("Error: CrstType 'Crst{0}' is declared as both unordered and in the same level as another CrstType", + crst.Name)); + return false; + } + + // Catch the simple cycle where the Crst type depends on itself. + if (crst.AcquiredBeforeList.Contains(crst)) + { + Console.WriteLine(String.Format("Error: CrstType 'Crst{0}' is declared as being acquired before itself", + crst.Name)); + return false; + } + + // Look for deeper cycles using a recursive algorithm in 'FindCycle()'. + List<CrstType> cycleList = new List<CrstType>(); + if (FindCycle(crst, crst, cycleList)) + { + Console.WriteLine(String.Format("Error: CrstType 'Crst{0}' is involved in a dependency cycle with the following CrstTypes:", + crst.Name)); + foreach (CrstType cycleCrst in cycleList) + Console.WriteLine(String.Format(" Crst{0}", cycleCrst.Name)); + return false; + } + } + + // Perform normalization of each set of Crst types that are included in the same group (i.e. have a + // 'SameLevelAs' relationship). Normalization means that each Crst type in a group will have exactly + // the same set of dependency rules as all the others. + CrstTypeGroup.NormalizeAllRules(); + + // The normalization process could have introduced cycles in the dependency graph so run the cycle + // detection pass again. We do separate passes like this since normalizing can lead to less intuitive + // error messages if a cycle is found: so if the cycle exists before normalization takes place we want + // to generate an error message then. + foreach (CrstType crst in m_crsts.Values) + { + List<CrstType> cycleList = new List<CrstType>(); + if (FindCycle(crst, crst, cycleList)) + { + Console.WriteLine(String.Format("Error: CrstType 'Crst{0}' is involved in a dependency cycle with the following CrstTypes:", + crst.Name)); + foreach (CrstType cycleCrst in cycleList) + Console.WriteLine(String.Format(" Crst{0}", cycleCrst)); + Console.WriteLine("Note that the cycle was detected only after 'SameLevelAs' processing was performed so some CrstType dependencies are implied by peer CrstTypes"); + return false; + } + } + + return true; + } + + // Recursively determine if a cycle exists in the Crst type dependency graph rooted at the 'rootCrst' + // type. The 'currCrst' indicates the next dependency to be examined (it will be the same as the + // 'rootCrst' when we're first called). The 'cycleList' argument contains a list of Crst types we've + // already examined in this branch of the algorithm and serves both to avoid checking the same node twice + // and to provide a list of the involved Crst types should a cycle be detected. + // Note that this algorithm is not designed to detect general cycles in the graph, only those that involve + // the 'rootCrst' directly. This is somewhat inefficient but gives us a simple way to generate clear error + // messages. + bool FindCycle(CrstType rootCrst, CrstType currCrst, List<CrstType> cycleList) + { + // Add the current Crst type to the list of those we've seen. + cycleList.Add(currCrst); + + // Look through all the dependencies of the current Crst type. + foreach (CrstType childCrst in currCrst.AcquiredBeforeList) + { + // If we find a reference back to the root Crst type then we've found a cycle. Start backing out + // from the recursion (keeping the list of nodes we visited intact) by returning true. + if (childCrst == rootCrst) + return true; + + // Otherwise iterate over the dependencies of the current node and for each one that we haven't + // already seen and recursively extend the search. + if (!cycleList.Contains(childCrst)) + if (FindCycle(rootCrst, childCrst, cycleList)) + return true; + } + + // Didn't find any cycles involving the root and this node; remove this node from the potential cycle + // list and return up to our caller indicating such. + cycleList.RemoveAt(cycleList.Count - 1); + + return false; + } + + // Topologically sort all the Crsts so we can assign a total ordering to them (in the form of a numeric + // ranking). Ranks start from 0 (Crst types that may be acquired at any time) and increment from there + // (Crst types that may only be acquired if a lower type is not already held). + // **** NOTE: The leveling process is destructive in that we will lose all dependency information from the + // Crst type definitions during the course of the algorithm. + void LevelCrsts() + { + // Note that Crst type dependency rules have been normalized (by the input parser) so that all + // AcquiredBefore/AcquiredAfter relationships have been reduced to AcquiredBefore relationships (i.e. + // any rule of the form "A AcquiredAfter B" has been converted to "B AcquiredBefore A". Any + // normalization makes the algorithm easier to program, but a normaliztion to AcquiredBefore + // relationships was chosen since it makes it particularly easy to implement an algorithm that assigns + // ranks beginning with zero and moving up to an arbitrary level. Any type that doesn't have any + // AcquiredBefore dependencies can always be ranked at a lower level than any remaining unranked types + // by definition and from this we can derive a simple iterative process to rank all the crst types. + + // Calculate how many Crst types we have left to rank (some are not included in this step because + // they've been marked as 'Unordered' in the input file). + int unsorted = 0; + foreach (CrstType crst in m_crsts.Values) + if (crst.Level == CrstType.CrstUnassigned) + unsorted++; + + // The ranking level we're going to assign to Crst types on the next pass of the algorithm. + int currLevel = 0; + + // Iterate while we still have Crst types left to rank. On each pass we'll assign a rank to those + // types that no longer have any dependencies forcing them to have a higher rank and then remove + // dependency rules involving those newly ranked types from the remaining types. + while (unsorted > 0) + { + // Record a flag indicating whether we manage to assign a rank to at least one Crst type on this + // pass. If we ever fail to do this we've hit a cycle (this is just paranoia, the Crst declaration + // validation performed in ValidateCrsts() should have detected such a cycle first). + bool madeProgress = false; + + // If we spot any types that are in a group (SameLevelAs relationship) then we defer assigning a + // rank till we've dealt with any non-group types (we wish to always place type groups in their + // very own rank else the Crst rank violation detection code won't detect violations between + // members of the group and singleton types that happened to be assigned rank on the same pass). + List<CrstTypeGroup> deferredGroups = new List<CrstTypeGroup>(); + + // Scan through all the Crst types. + foreach (CrstType crst in m_crsts.Values) + { + // Skip those types that already have a rank assigned. + if (crst.Level != CrstType.CrstUnassigned) + continue; + + // We're looking for Crst types that no longer have any types that can be acquired while they + // are already held. This indicates that it's safe to assign the current rank to them (since + // there are no remaining dependencies that need to be ranked first (i.e. with a lower rank + // value than this type). + if (crst.AcquiredBeforeList.Count == 0) + { + if (crst.Group == null) + { + // If this type is not part of the group we can go and assign the rank right away. + crst.Level = currLevel; + madeProgress = true; + unsorted--; + } + else if (!deferredGroups.Contains(crst.Group)) + // Otherwise we'll defer ranking this group member until all the singletons are + // processed. + deferredGroups.Add(crst.Group); + } + } + + // We've gone through the entire collection of Crst types and assigned the current rank level to + // any singleton Crst types that qualify. Now deal with any group members we detected (it's + // possible that more than one group qualifies for ranking at this level but we need to be careful + // to assign distinct rank values to each group to avoid hiding lock rank violations (since group + // members are always allowed to be acquired alongside any other type with the same rank value). + // Iterate over each distinct group that we found in this pass. + foreach (CrstTypeGroup group in deferredGroups) + { + // Look at our progress flag here. If it is false then we didn't have any singleton Crst types + // ranked at this level and we haven't processed any other groups at this level either. Thus + // we can rank this group at the current level. Otherwise at least one type was already ranked + // with this level so we need to increment to a new, distinct level to avoid ranking + // ambiguity. + if (madeProgress) + currLevel++; + + // Iterate through each Crst type that is a member of this group assigning them the (same) + // current rank. + foreach (CrstType crst in group.Members) + { + // Double check that each member has the same dependencies (i.e. they should all be empty + // by now). There should be no way that this error should ever occur, it's just paranoia + // on my part. + if (crst.AcquiredBeforeList.Count != 0) + throw new Exception("Internal error: SameLevel CrstTypes with differing rulesets"); + + crst.Level = currLevel; + unsorted--; + } + + // Once we've processed at least one group we've made progress this iteration. + madeProgress = true; + } + + // If we didn't manage to assign rank to at least one Crst type then we're not going to do any + // better next iteration either (because no state was updated in this iteration). This should only + // occur in the presence of a dependency cycle and we shouldn't get that here after a successful + // call to ValidateCrsts(), so this check is pure paranoia. + if (!madeProgress) + { + Console.WriteLine(String.Format("{0} unsorted remain", unsorted)); + throw new Exception("Cycle detected trying to assign level " + currLevel.ToString()); + } + + // Loop through all the unranked Crsts types and remove any AcquiredBefore relationships that + // involve types we've already leveled (since those types, by definition, have already been + // assigned a lower rank). + foreach (CrstType crst in m_crsts.Values) + { + if (crst.Level != CrstType.CrstUnassigned) + continue; + List<CrstType> prunedCrsts = crst.AcquiredBeforeList.FindAll(Unleveled); + crst.AcquiredBeforeList = prunedCrsts; + } + + // Done with this rank level, move to the next. + currLevel++; + } + } + + // Predicate method used with List<T>.FindAll() to locate Crst types that haven't had their rank assigned + // yet. + static bool Unleveled(CrstType crst) + { + return crst.Level == CrstType.CrstUnassigned; + } +} + +// Class used to parse a CrstTypes.def file into a dictionary of Crst type definitions. It uses a simple lexer +// that removes comments then forms tokens out of any consecutive non-whitespace characters. An equally simple +// recursive descent parser forms Crst instances by parsing the token stream. +class TypeFileParser +{ + // Remember the input file name and the dictionary we're meant to populate. + string m_typeFileName; + Dictionary<string, CrstType> m_crsts; + + // Compile regular expressions for detecting comments and tokens in the parser input. + Regex m_commentRegex = new Regex(@"//.*"); + Regex m_tokenRegex = new Regex(@"^(\s*(\S+)\s*)*"); + + // Input is lexed into an array of tokens. We record the index of the token being currently parsed. + Token[] m_tokens; + int m_currToken; + + // Parse the given file into Crst type definitions and place these definitions in the dictionary provided. + // Syntax errors are signalled via ParseError derived exceptions. + public void ParseFile(string typeFileName, Dictionary<string, CrstType> crsts) + { + m_typeFileName = typeFileName; + m_crsts = crsts; + + // Lex the file into tokens. + InitTokenStream(); + + // Parse the tokens according to the grammar set out at the top of this file. + // Loop until we have no further tokens to process. + while (!IsEof()) + { + // Grab the next token. + Token token = NextToken(); + + // We're at the top level, so the token had better be 'Crst'. + if (token.Id != KeywordId.Crst) + throw new UnexpectedTokenError(token, KeywordId.Crst); + + // OK, parse the rest of this single Crst type definition. + ParseCrst(); + } + } + + // Parse a single Crst type definition. + void ParseCrst() + { + // The next token had better be an identifier (the Crst type name). + Token token = NextToken(); + if (token.Id != KeywordId.Id) + throw new UnexpectedTokenError(token, KeywordId.Id); + + // The Crst instance might already exist in the dictionary (forward references to a Crst type cause + // these entries to auto-vivify). But in that case the entry better not be marked as 'Defined' which + // would indicate a double declaration. + CrstType crst; + if (m_crsts.ContainsKey(token.Text)) + { + crst = m_crsts[token.Text]; + if (crst.Defined) + throw new ParseError(String.Format("Duplicate definition for CrstType '{0}'", token.Text), token); + } + else + { + // Otherwise this Crst type hasn't been seen thus far so we allocate a new instance and add it to + // the dictionary. + crst = new CrstType(token.Text); + m_crsts.Add(crst.Name, crst); + } + + // We're defining, not just referencing this type. + crst.Defined = true; + + // Parse any attributes inside this definition (until we see an 'End' token). + bool parsingCrst = true; + while (parsingCrst) + { + // Get the next token. Either some attribute keyword or 'End'. + token = NextToken(); + List<CrstType> list; + + switch (token.Id) + { + + case KeywordId.AcquiredBefore: + // Simply parse the following list of Crst types into the current type's AcquiredBefore list. + ParseList(crst.AcquiredBeforeList); + break; + + case KeywordId.AcquiredAfter: + // AcquiredAfter is trickier. To make the ranking algorithm's life easier we actually + // normalize all rules to the AcquiredBefore form (see LevelCrsts() for the reasoning). So we + // capture the list of Crst types that follow the AcquiredAfter keyword and then append the + // current type to the AcquiredBefore list of each type found. + list = new List<CrstType>(); + ParseList(list); + foreach (CrstType priorCrst in list) + priorCrst.AcquiredBeforeList.Add(crst); + break; + + case KeywordId.SameLevelAs: + // Parse the following list of Crst types them let the CrstTypeGroup class handle the + // resulting updates to the type groups we're currently maintaining. See the comments for the + // CrstTypeGroup class for more details. + list = new List<CrstType>(); + ParseList(list); + foreach (CrstType sameLevelCrst in list) + CrstTypeGroup.Join(crst, sameLevelCrst); + break; + + case KeywordId.Unordered: + crst.Level = CrstType.CrstUnordered; + break; + + case KeywordId.End: + parsingCrst = false; + break; + + default: + throw new UnexpectedTokenError(token, + KeywordId.AcquiredBefore, + KeywordId.AcquiredAfter, + KeywordId.SameLevelAs, + KeywordId.Unordered); + } + } + } + + // Parse a list of Crst type names. Any other token terminates the list (without error and without + // consuming that token from the stream). The list of tokens is returned as a list of corresponding + // CrstTypes (which are auto-vivified in the output dictionary if they haven't been declared yet). + void ParseList(List<CrstType> list) + { + // Parse tokens until we find a non-indentifier. + while (true) + { + Token token = NextToken(); + if (token.Id != KeywordId.Id) + { + // We found the list terminator. Push the non-identifier token back into the stream for our + // caller to parse correctly. + UnwindToken(); + return; + } + + // Look up or add a new CrstType corresponding to this type name. + CrstType crst; + if (m_crsts.ContainsKey(token.Text)) + crst = m_crsts[token.Text]; + else + { + crst = new CrstType(token.Text); + m_crsts[crst.Name] = crst; + } + + // Add the type to the output list we're building. + list.Add(crst); + } + } + + // Lex the input file into an array of tokens. + void InitTokenStream() + { + StreamReader file = new StreamReader(m_typeFileName); + int lineNumber = 1; + List<Token> tokenList = new List<Token>(); + + // Read the file a line at a time. + string line; + while ((line = file.ReadLine()) != null) + { + // Remove comments from the current line. + line = m_commentRegex.Replace(line, ""); + + // Match all contiguous non-whitespace characters as individual tokens. + Match match = m_tokenRegex.Match(line); + if (match.Success) + { + // For each token captured build a token instance and record the token text and the file, line + // and column at which it was encountered (these latter in order to produce useful syntax + // error messages). + CaptureCollection cap = match.Groups[2].Captures; + for (int i = 0; i < cap.Count; i++) + tokenList.Add(new Token(m_typeFileName, cap[i].Value, lineNumber, cap[i].Index)); + } + + lineNumber++; + } + + // Record the list of tokens we captured as an array and reset the index of the next token to be + // handled by the parser. + m_tokens = tokenList.ToArray(); + m_currToken = 0; + } + + // Have we run out of tokens to parse? + bool IsEof() + { + return m_currToken >= m_tokens.Length; + } + + // Get the next token and throw an exception if we ran out. + Token NextToken() + { + if (m_currToken >= m_tokens.Length) + throw new UnexpectedEofError(); + return m_tokens[m_currToken++]; + } + + // Push the last token parsed back into the stream. + void UnwindToken() + { + if (m_currToken <= 0) + throw new InvalidOperationException(); + m_currToken--; + } + + // The various keywords we can encounter (plus Id for identifiers, which are currently always Crst type + // names). + internal enum KeywordId + { + Id, + Crst, + End, + AcquiredBefore, + AcquiredAfter, + Unordered, + SameLevelAs, + } + + // Class encapsulating a single token captured from the input file. + internal class Token + { + // Hash of keyword text to enum values. + static Dictionary<string, KeywordId> s_keywords; + + // The characters comprising the text of the token from the input file. + string m_text; + + // Where the token was found (for error messages). + string m_file; + int m_line; + int m_column; + + // The ID of the keyword this token represents (or KeywordId.Id). + KeywordId m_id; + + // Static class initialization. + static Token() + { + // Populate the keyword hash. No sense building complex finite state machines to improve the + // efficiency of keyword lexing here since the input file (and keyword set) is never going to be + // big enough to justify the extra work. + s_keywords = new Dictionary<string, KeywordId>(); + s_keywords.Add("crst", KeywordId.Crst); + s_keywords.Add("end", KeywordId.End); + s_keywords.Add("acquiredbefore", KeywordId.AcquiredBefore); + s_keywords.Add("acquiredafter", KeywordId.AcquiredAfter); + s_keywords.Add("unordered", KeywordId.Unordered); + s_keywords.Add("samelevelas", KeywordId.SameLevelAs); + } + + public Token(string file, string text, int line, int column) + { + m_file = file; + m_text = text; + m_line = line; + m_column = column; + + // Map token text to keyword ID. True keywords (not identifiers) are case insensitive so normalize + // the text to lower case before performing the keyword hash lookup. + string canonName = m_text.ToLower(); + if (s_keywords.ContainsKey(canonName)) + m_id = s_keywords[canonName]; + else + m_id = KeywordId.Id; + } + + public string Text {get { return m_text; }} + public string Location {get { return String.Format("{0} line {1}, column {2}", m_file, m_line, m_column); }} + public KeywordId Id {get { return m_id; }} + } + + // Base class for all syntax errors reported by the parser. + internal class ParseError : Exception + { + // A raw error message. + public ParseError(string message) + : base(message) + {} + + // An error message tagged with a file, line and column (coming from an error token). + public ParseError(string message, Token errorToken) + : base(String.Format("{0}: {1}", errorToken.Location, message)) + {} + + // Produce a textual name for the given keyword type. + protected static string IdToName(KeywordId id) + { + if (id == KeywordId.Id) + return "a CrstType name"; + return String.Format("'{0}'", id.ToString()); + } + } + + // Syntax error used when an unexpected token is encountered which further lists the valid tokens that + // would otherwise have been accepted. + internal class UnexpectedTokenError : ParseError + { + // Produce an unexpected token message with a file, line and column coming from an error token and + // optionally the names of zero or more tokens that would have been accepted. + public UnexpectedTokenError(Token errorToken, params KeywordId[] expected) + : base(FormatErrorMessage(errorToken, expected)) + {} + + static string FormatErrorMessage(Token errorToken, KeywordId[] expected) + { + StringBuilder message = new StringBuilder(String.Format("Unexpected token '{0}' at {1}", + errorToken.Text, errorToken.Location)); + if (expected.Length == 0) + { + } + else if (expected.Length == 1) + { + message.Append(String.Format("; expected {0}", IdToName(expected[0]))); + } + else + { + message.Append("; expected one of "); + for (int i = 0; i < expected.Length - 1; i++) + message.Append(String.Format("{0}, ", IdToName(expected[i]))); + message.Append(IdToName(expected[expected.Length - 1])); + + } + + return message.ToString(); + } + } + + // Syntax error used when we unexpectedly ran out of tokens. + internal class UnexpectedEofError : ParseError + { + public UnexpectedEofError() + : base("Unexpected end of file") + {} + } +} + +// This class represents an instance of a Crst type. These are unqiuely identified by case-sensitive name (the +// same as the enum name used in vm code, minus the 'Crst' prefix). +class CrstType : IComparable +{ + // Special level constants used to indicate unordered Crst types or those types we haven't gotten around + // to ranking yet. + public static readonly int CrstUnordered = -1; + public static readonly int CrstUnassigned = -2; + + // Name of the type, e.g. "AppDomainCache" for the CrstAppDomainCache type. + string m_name; + + // The numeric ranking assigned to this type. Starts as CrstUnassigned and then becomes either + // CrstUnordered (while parsing the input file) or a number >= 0 (during LevelCrsts()). + int m_level; + + // List of Crst types that can be legally acquired while this one is held. (AcquiredAfter relationships + // are by switching the terms and adding to the second type's AcquiredBefore list). + List<CrstType> m_acquiredBeforeCrsts; + + // Either null if this Crst type is not in (or has not yet been determined to be in) a SameLevelAs + // relationship or points to a CrstTypeGroup that records all the sibling types at the same level (that + // have been discovered thus far during parsing). + CrstTypeGroup m_group; + + // Set once a definition for this type has been discovered. Used to detect double definitions and types + // referenced without definitions. + bool m_defined; + + public CrstType(string name) + { + m_name = name; + m_level = CrstUnassigned; + m_acquiredBeforeCrsts = new List<CrstType>(); + m_group = null; + m_defined = false; + } + + public string Name {get { return m_name; }} + public int Level {get { return m_level; } set { m_level = value; }} + public List<CrstType> AcquiredBeforeList {get { return m_acquiredBeforeCrsts; } set { m_acquiredBeforeCrsts = value; }} + public CrstTypeGroup Group {get { return m_group; } set { m_group = value; }} + public bool Defined {get {return m_defined; } set { m_defined = value; }} + + // Helper used to sort CrstTypes. The sort order is lexical based on the type name. + public int CompareTo(object other) + { + return m_name.CompareTo(((CrstType)other).m_name); + } +} + +// Every time a SameLevelAs relationship is used we need to be careful to keep track of the transitive closure +// of all types bound in the relationship. That's because such a relationship impacts the other dependency +// rules (each member of a SameLevelAs group must behave as though it has exactly the same dependency rules as +// all the others). Identifying all the members is tricky because "A SameLevelAs B" and "B SameLevelAs C" +// implies "A SameLevelAs C". So we use a separate tracking structure, instances of the CrstTypeGroup type, to +// do the bookkeeping for us. Each Crst type belongs to either zero or one CrstTypeGroups. As we find new +// SameLevelAs relationships we create new groups, add types to existing groups or merge groups (as previous +// distinct groups are merged by the discovery of a SameLevelAs relationship that links them). By the time +// parsing has finished we are guaranteed to have discovered all the distinct, disjoint groups and to have +// fully populated them with the transitive closure of all related types. We can them normalize all groups +// members so they share the same AcquiredBefore relationships. +class CrstTypeGroup +{ + // We record every group that has been formed so far. This makes normalizing all groups easier. + static List<CrstTypeGroup> s_groups = new List<CrstTypeGroup>(); + + // Crst types that are members of the current group. There are no duplicates in this list. + List<CrstType> m_members = new List<CrstType>(); + + // Declare a SameLevelAs relationship between the two Crst types given. Groups will be assigned, created + // or merged as required to maintain our guarantees (each CrstType is a member of at most one group and + // all CrstTypes involved in the same transitive closure of a SameLevelAs relationship are members of one + // group). + public static void Join(CrstType crst1, CrstType crst2) + { + CrstTypeGroup group; + + if (crst1 == crst2) + { + // In this case the type refers to itself. Create a singleton group for this type if it doesn't + // already exist. + if (crst1.Group == null) + { + group = new CrstTypeGroup(); + group.m_members.Add(crst1); + + s_groups.Add(group); + + crst1.Group = group; + } + } + else if (crst1.Group == null && crst2.Group == null) + { + // Neither types belong to a group already. So we can create a new one and add both types to it. + group = new CrstTypeGroup(); + group.m_members.Add(crst1); + group.m_members.Add(crst2); + + s_groups.Add(group); + + crst1.Group = group; + crst2.Group = group; + } + else if (crst1.Group == null) + { + // The first type doesn't belong to a group yet but the second does. So we can simply add the + // first type to the second group. + group = crst2.Group; + group.m_members.Add(crst1); + + crst1.Group = group; + } + else if (crst2.Group == null) + { + // As for the case above but the group/no-group positions are reversed. + group = crst1.Group; + group.m_members.Add(crst2); + + crst2.Group = group; + } + else if (crst1.Group != crst2.Group) + { + // Both types belong to different groups so we'll have to merge them. Add the members of group 2 + // to group 1 and throw away group 2. + group = crst1.Group; + CrstTypeGroup absorbGroup = crst2.Group; + foreach (CrstType crst in absorbGroup.m_members) + { + group.m_members.Add(crst); + crst.Group = group; + } + + s_groups.Remove(absorbGroup); + } + + // The only case left is when both types are already in the same group and there's no work needed in + // this case. + } + + // Normalize all the groups we created during parsing. See below for the definition of normalization. + public static void NormalizeAllRules() + { + foreach (CrstTypeGroup group in s_groups) + group.NormalizeRules(); + } + + // Normalize this group. This involves adjusting the AcquiredBefore list of each member to be the union of + // all such rules within the group. This step allows us to detect cycles in the dependency graph that + // would otherwise remain hidden if we only examined the unnormalized AcquiredBefore rules. + void NormalizeRules() + { + // This local will contain the union of all AcquiredBefore rules. + List<CrstType> acquiredBeforeList = new List<CrstType>(); + + // Iterate through each member of the group. + foreach (CrstType crst in m_members) + { + // Add each AcquiredBefore rule we haven't already seen to the union. + foreach (CrstType afterCrst in crst.AcquiredBeforeList) + if (!acquiredBeforeList.Contains(afterCrst)) + acquiredBeforeList.Add(afterCrst); + } + + // Reset each member's AcquiredBefore list to a copy of the union we calculated. Note it's important + // to make a (shallow) copy because the ranking process modifies this list and so a shared copy would + // cause unexpected results. + foreach (CrstType crst in m_members) + crst.AcquiredBeforeList = acquiredBeforeList.GetRange(0, acquiredBeforeList.Count); + } + + public List<CrstType> Members {get { return m_members; }} +} |