Program.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using System;
  2. using System.Collections.Generic;
  3. using Newtonsoft.Json;
  4. using System.Runtime.Serialization;
  5. namespace Yarn
  6. {
  7. // An exception representing something going wrong during parsing
  8. [Serializable]
  9. internal class ParseException : Exception
  10. {
  11. internal int lineNumber = 0;
  12. internal static ParseException Make(Token foundToken, params TokenType[] expectedTypes)
  13. {
  14. var lineNumber = foundToken.lineNumber + 1;
  15. var expectedTypeNames = new List<String>();
  16. foreach (var type in expectedTypes)
  17. {
  18. expectedTypeNames.Add(type.ToString());
  19. }
  20. string possibleValues = string.Join(",", expectedTypeNames.ToArray());
  21. string message = string.Format("Line {0}:{1}: Expected {2}, but found {3}",
  22. lineNumber,
  23. foundToken.columnNumber,
  24. possibleValues,
  25. foundToken.type.ToString()
  26. );
  27. var e = new ParseException(message);
  28. e.lineNumber = lineNumber;
  29. return e;
  30. }
  31. internal static ParseException Make(Token mostRecentToken, string message)
  32. {
  33. var lineNumber = mostRecentToken.lineNumber + 1;
  34. string theMessage = string.Format("Line {0}:{1}: {2}",
  35. lineNumber,
  36. mostRecentToken.columnNumber,
  37. message);
  38. var e = new ParseException(theMessage);
  39. e.lineNumber = lineNumber;
  40. return e;
  41. }
  42. internal static ParseException Make(Antlr4.Runtime.ParserRuleContext context, string message)
  43. {
  44. int line = context.Start.Line;
  45. // getting the text that has the issue inside
  46. int start = context.Start.StartIndex;
  47. int end = context.Stop.StopIndex;
  48. string body = context.Start.InputStream.GetText(new Antlr4.Runtime.Misc.Interval(start, end));
  49. string theMessage = String.Format("Error on line {0}\n{1}\n{2}",line,body,message);
  50. var e = new ParseException(theMessage);
  51. e.lineNumber = line;
  52. return e;
  53. }
  54. internal ParseException(string message) : base(message) { }
  55. }
  56. internal struct LineInfo
  57. {
  58. public int lineNumber;
  59. public string nodeName;
  60. public LineInfo(string nodeName, int lineNumber)
  61. {
  62. this.nodeName = nodeName;
  63. this.lineNumber = lineNumber;
  64. }
  65. }
  66. [JsonObject(MemberSerialization.OptIn)] // properties must opt-in to JSON serialization
  67. internal class Program
  68. {
  69. internal Dictionary<string, string> strings = new Dictionary<string, string>();
  70. internal Dictionary<string, LineInfo> lineInfo = new Dictionary<string, LineInfo>();
  71. [JsonProperty]
  72. internal Dictionary<string, Node> nodes = new Dictionary<string, Node>();
  73. // When saving programs, we want to save only lines that do NOT have a line: key.
  74. // This is because these lines will be loaded from a string table.
  75. // However, because certain strings (like those used in expressions) won't have tags,
  76. // they won't be included in generated string tables, so we need to export them here.
  77. // We do this by NOT including the main strings list, and providing a property
  78. // that gets serialised as "strings" in the output, which includes all untagged strings.
  79. [JsonProperty("strings")]
  80. internal Dictionary<string, string> untaggedStrings
  81. {
  82. get
  83. {
  84. var result = new Dictionary<string, string>();
  85. foreach (var line in strings)
  86. {
  87. if (line.Key.StartsWith("line:"))
  88. {
  89. continue;
  90. }
  91. result.Add(line.Key, line.Value);
  92. }
  93. return result;
  94. }
  95. }
  96. private int stringCount = 0;
  97. /// Loads a new string table into the program.
  98. /** The string table is merged with any existing strings,
  99. * with the new table taking precedence over the old.
  100. */
  101. public void LoadStrings(Dictionary<string, string> newStrings)
  102. {
  103. foreach (var entry in newStrings)
  104. {
  105. strings[entry.Key] = entry.Value;
  106. }
  107. }
  108. public string RegisterString(string theString, string nodeName, string lineID, int lineNumber, bool localisable)
  109. {
  110. string key;
  111. if (lineID == null)
  112. key = string.Format("{0}-{1}", nodeName, stringCount++);
  113. else
  114. key = lineID;
  115. // It's not in the list; append it
  116. strings.Add(key, theString);
  117. if (localisable)
  118. {
  119. // Additionally, keep info about this string around
  120. lineInfo.Add(key, new LineInfo(nodeName, lineNumber));
  121. }
  122. return key;
  123. }
  124. public string GetString(string key)
  125. {
  126. string value = null;
  127. strings.TryGetValue(key, out value);
  128. return value;
  129. }
  130. public string DumpCode(Library l)
  131. {
  132. var sb = new System.Text.StringBuilder();
  133. foreach (var entry in nodes)
  134. {
  135. sb.AppendLine("Node " + entry.Key + ":");
  136. int instructionCount = 0;
  137. foreach (var instruction in entry.Value.instructions)
  138. {
  139. string instructionText;
  140. if (instruction.operation == ByteCode.Label)
  141. {
  142. instructionText = instruction.ToString(this, l);
  143. }
  144. else
  145. {
  146. instructionText = " " + instruction.ToString(this, l);
  147. }
  148. string preface;
  149. if (instructionCount % 5 == 0 || instructionCount == entry.Value.instructions.Count - 1)
  150. {
  151. preface = string.Format("{0,6} ", instructionCount);
  152. }
  153. else
  154. {
  155. preface = string.Format("{0,6} ", " ");
  156. }
  157. sb.AppendLine(preface + instructionText);
  158. instructionCount++;
  159. }
  160. sb.AppendLine();
  161. }
  162. sb.AppendLine("String table:");
  163. foreach (var entry in strings)
  164. {
  165. var lineInfo = this.lineInfo[entry.Key];
  166. sb.AppendLine(string.Format("{0}: {1} ({2}:{3})", entry.Key, entry.Value, lineInfo.nodeName, lineInfo.lineNumber));
  167. }
  168. return sb.ToString();
  169. }
  170. public string GetTextForNode(string nodeName)
  171. {
  172. return this.GetString(nodes[nodeName].sourceTextStringID);
  173. }
  174. public void Include(Program otherProgram)
  175. {
  176. foreach (var otherNodeName in otherProgram.nodes)
  177. {
  178. if (nodes.ContainsKey(otherNodeName.Key))
  179. {
  180. throw new InvalidOperationException(string.Format("This program already contains a node named {0}", otherNodeName.Key));
  181. }
  182. nodes[otherNodeName.Key] = otherNodeName.Value;
  183. }
  184. foreach (var otherString in otherProgram.strings)
  185. {
  186. if (nodes.ContainsKey(otherString.Key))
  187. {
  188. throw new InvalidOperationException(string.Format("This program already contains a string with key {0}", otherString.Key));
  189. }
  190. strings[otherString.Key] = otherString.Value;
  191. }
  192. }
  193. }
  194. internal class Node
  195. {
  196. public List<Instruction> instructions = new List<Instruction>();
  197. public string name;
  198. /// the entry in the program's string table that contains
  199. /// the original text of this node; null if this is not available
  200. public string sourceTextStringID = null;
  201. public Dictionary<string, int> labels = new Dictionary<string, int>();
  202. public List<string> tags;
  203. }
  204. struct Instruction
  205. {
  206. public ByteCode operation;
  207. public object operandA;
  208. public object operandB;
  209. public string ToString(Program p, Library l)
  210. {
  211. // Labels are easy: just dump out the name
  212. if (operation == ByteCode.Label)
  213. {
  214. return operandA + ":";
  215. }
  216. // Convert the operands to strings
  217. var opAString = operandA != null ? operandA.ToString() : "";
  218. var opBString = operandB != null ? operandB.ToString() : "";
  219. // Generate a comment, if the instruction warrants it
  220. string comment = "";
  221. // Stack manipulation comments
  222. var pops = 0;
  223. var pushes = 0;
  224. switch (operation)
  225. {
  226. // These operations all push a single value to the stack
  227. case ByteCode.PushBool:
  228. case ByteCode.PushNull:
  229. case ByteCode.PushNumber:
  230. case ByteCode.PushString:
  231. case ByteCode.PushVariable:
  232. case ByteCode.ShowOptions:
  233. pushes = 1;
  234. break;
  235. // Functions pop 0 or more values, and pop 0 or 1
  236. case ByteCode.CallFunc:
  237. var function = l.GetFunction((string)operandA);
  238. pops = function.paramCount;
  239. if (function.returnsValue)
  240. pushes = 1;
  241. break;
  242. // Pop always pops a single value
  243. case ByteCode.Pop:
  244. pops = 1;
  245. break;
  246. // Switching to a different node will always clear the stack
  247. case ByteCode.RunNode:
  248. comment += "Clears stack";
  249. break;
  250. }
  251. // If we had any pushes or pops, report them
  252. if (pops > 0 && pushes > 0)
  253. comment += string.Format("Pops {0}, Pushes {1}", pops, pushes);
  254. else if (pops > 0)
  255. comment += string.Format("Pops {0}", pops);
  256. else if (pushes > 0)
  257. comment += string.Format("Pushes {0}", pushes);
  258. // String lookup comments
  259. switch (operation)
  260. {
  261. case ByteCode.PushString:
  262. case ByteCode.RunLine:
  263. case ByteCode.AddOption:
  264. // Add the string for this option, if it has one
  265. if ((string)operandA != null)
  266. {
  267. var text = p.GetString((string)operandA);
  268. comment += string.Format("\"{0}\"", text);
  269. }
  270. break;
  271. }
  272. if (comment != "")
  273. {
  274. comment = "; " + comment;
  275. }
  276. return string.Format("{0,-15} {1,-10} {2,-10} {3, -10}", operation.ToString(), opAString, opBString, comment);
  277. }
  278. }
  279. internal enum ByteCode
  280. {
  281. /// opA = string: label name
  282. Label,
  283. /// opA = string: label name
  284. JumpTo,
  285. /// peek string from stack and jump to that label
  286. Jump,
  287. /// opA = int: string number
  288. RunLine,
  289. /// opA = string: command text
  290. RunCommand,
  291. /// opA = int: string number for option to add
  292. AddOption,
  293. /// present the current list of options, then clear the list; most recently selected option will be on the top of the stack
  294. ShowOptions,
  295. /// opA = int: string number in table; push string to stack
  296. PushString,
  297. /// opA = float: number to push to stack
  298. PushNumber,
  299. /// opA = int (0 or 1): bool to push to stack
  300. PushBool,
  301. /// pushes a null value onto the stack
  302. PushNull,
  303. /// opA = string: label name if top of stack is not null, zero or false, jumps to that label
  304. JumpIfFalse,
  305. /// discard top of stack
  306. Pop,
  307. /// opA = string; looks up function, pops as many arguments as needed, result is pushed to stack
  308. CallFunc,
  309. /// opA = name of variable to get value of and push to stack
  310. PushVariable,
  311. /// opA = name of variable to store top of stack in
  312. StoreVariable,
  313. /// stops execution
  314. Stop,
  315. /// run the node whose name is at the top of the stack
  316. RunNode,
  317. /// Pop two strings, concat them, and push back to the stack
  318. Concat,
  319. /// Run a line, but get the text from the stack instead of the table.
  320. RunLineFromStack,
  321. }
  322. }