Commits

Jesse McGrew  committed 155e05e

Added Analyzer, vocab classes, target classes, message facility

  • Participants
  • Parent commits 22244c2

Comments (0)

Files changed (16)

File Rellor.Core/Analyzer.cs

+using System.Diagnostics.Contracts;
+
+namespace Rellor.Core
+{
+    /// <summary>
+    /// Encapsulates the lexer, parser, and other front end components.
+    /// </summary>
+    public class Analyzer
+    {
+        public Analyzer()
+        {
+            this.GlobalScope = new GlobalScope();
+        }
+
+        [ContractInvariantMethod]
+        private void ObjectInvariant()
+        {
+            Contract.Invariant(this.GlobalScope != null);
+        }
+
+        public IGlobalScope GlobalScope { get; set; }
+    }
+}

File Rellor.Core/ITargetCapabilities.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+
+namespace Rellor.Core
+{
+    [Flags]
+    public enum TargetTextSupport
+    {
+        None = 0,
+
+        /// <summary>
+        /// Target supports the "print", "print_ret", "new_line", "spaces", and "inversion" statements.
+        /// </summary>
+        Print = 1,
+        /// <summary>
+        /// Target supports the "read" statement.
+        /// </summary>
+        Read = 2,
+        /// <summary>
+        /// Target supports the "box" statement.
+        /// </summary>
+        Box = 4,
+        /// <summary>
+        /// Target supports the "font" statement.
+        /// </summary>
+        Font = 8,
+        /// <summary>
+        /// Target supports the "style" statement.
+        /// </summary>
+        Style = 16,
+
+        All = Print + Read + Box + Font + Style
+    }
+
+    [Flags]
+    public enum PersistenceSupport
+    {
+        None = 0,
+
+        SaveRestore = 1,
+        Undo = 2,
+
+        All = SaveRestore + Undo
+    }
+
+    [Flags]
+    public enum SystemSupport
+    {
+        None = 0,
+
+        /// <summary>
+        /// Target provides a memory area that can't be modified.
+        /// </summary>
+        ROM = 1,
+        /// <summary>
+        /// Target can clean up the stack at the end of a routine without knowing how full it is.
+        /// </summary>
+        StackCleanup = 2,
+        /// <summary>
+        /// Target can handle inline assembly opcodes.
+        /// </summary>
+        Assembly = 4,
+        // TODO: extern functions?
+        // TODO: global stack? (passed between routines)
+    }
+
+    [ContractClass(typeof(ITargetCapabilitiesContract))]
+    public interface ITargetCapabilities
+    {
+        /// <summary>
+        /// The human-readable name, such as "Z-machine version 5". Used in target-related messages.
+        /// </summary>
+        string Name { get; }
+        /// <summary>
+        /// The name(s) of one or more constants to define during compilation, separated by
+        /// semicolons, such as "TARGET_ZCODE;TARGET_Z5".
+        /// </summary>
+        string ConditionalConstant { get; }
+
+        /// <summary>
+        /// A bit field describing the target's support for text I/O and formatting statements.
+        /// </summary>
+        TargetTextSupport TextStatementSupport { get; }
+        /// <summary>
+        /// A bit field describing the target's support for persistence statements.
+        /// </summary>
+        PersistenceSupport PersistenceSupport { get; }
+
+        /// <summary>
+        /// The word size, which must be 4 or 8 bytes.
+        /// </summary>
+        int WordSize { get; }
+        /// <summary>
+        /// The default grammar version, which must be 1 or 2.
+        /// </summary>
+        int DefaultGrammarVersion { get; }
+        /// <summary>
+        /// The size of a dictionary word in bytes.
+        /// </summary>
+        int? DictWordSize { get; }
+        /// <summary>
+        /// The number of bytes available for object attributes (i.e. the number of supported
+        /// attributes divided by 8), or null if no limit.
+        /// </summary>
+        int? NumAttrBytes { get; }
+        /// <summary>
+        /// The number of common properties available, or null if no limit.
+        /// </summary>
+        int? NumCommonProps { get; }
+        /// <summary>
+        /// The first property number for individual properties.
+        /// </summary>
+        int IndivPropStart { get; }
+
+        /// <summary>
+        /// Gets a comparer that can hash and compare words according to the target's dictionary encoding rules.
+        /// </summary>
+        IEqualityComparer<string> DictionaryComparer { get; }
+    }
+
+    [ContractClassFor(typeof(ITargetCapabilities))]
+    internal abstract class ITargetCapabilitiesContract : ITargetCapabilities
+    {
+        [ContractInvariantMethod]
+        private void ObjectInvariant()
+        {
+            Contract.Invariant(!string.IsNullOrEmpty(Name));
+            Contract.Invariant(WordSize == 2 || WordSize == 4);
+            Contract.Invariant(DefaultGrammarVersion >= 1 && DefaultGrammarVersion <= 2);
+            Contract.Invariant(DictWordSize == null || DictWordSize > 0);
+            Contract.Invariant(!string.IsNullOrEmpty(ConditionalConstant));
+            Contract.Invariant(NumAttrBytes == null || NumAttrBytes > 0);
+            Contract.Invariant(NumCommonProps == null || NumCommonProps > 0);
+            Contract.Invariant(IndivPropStart >= 0);
+            Contract.Invariant(DictionaryComparer != null);
+        }
+
+        public abstract string Name { get; }
+        public abstract int WordSize { get; }
+        public abstract string ConditionalConstant { get; }
+        public abstract TargetTextSupport TextStatementSupport { get; }
+        public abstract PersistenceSupport PersistenceSupport { get; }
+        public abstract int DefaultGrammarVersion { get; }
+        public abstract int? DictWordSize { get; }
+        public abstract int? NumAttrBytes { get; }
+        public abstract int? NumCommonProps { get; }
+        public abstract int IndivPropStart { get; }
+        public abstract IEqualityComparer<string> DictionaryComparer { get; }
+    }
+}

File Rellor.Core/ITargetValidation.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+
+namespace Rellor.Core
+{
+    [ContractClass(typeof(ITargetValidationContract))]
+    public interface ITargetValidation
+    {
+    }
+
+    [ContractClassFor(typeof(ITargetValidation))]
+    internal abstract class ITargetValidationContract : ITargetValidation
+    {
+
+    }
+}

File Rellor.Core/Inform6.g3

 /**************************** TOP-LEVEL DIRECTIVES ****************************/
 
 public
-program
-scope { IScope globalScope; }
+program[IGlobalScope suppliedGlobalScope]
+scope { IGlobalScope globalScope; }
 scope Symbols, Quirks;
-@init { $program::globalScope = $Symbols::scope = new GlobalScope(); }
+@init { $Symbols::scope = $program::globalScope = $suppliedGlobalScope ?? CreateGlobalScope(); }
     :	(HASH!? directive[false])*
     ;
 
 
 for_stmt
 	:	kFOR '(' for_exprs ')' statement
+		{if ($for_exprs.used_semicolon != null)
+			logger.Log(CoreMessages.SemicolonUsedInFor, $for_exprs.used_semicolon);}
 		-> ^(kFOR for_exprs statement)
 	;
 
-for_exprs returns [bool used_semicolon]
+for_exprs returns [RellorToken used_semicolon]
 scope Quirks;
 	:	{$Quirks::no_superclass = true;}
 		(	init=any_expr
 			-> OMITTED
 		)
 		{$Quirks::no_superclass = false;}
-		(	('::' | (':' | ';' {$used_semicolon = true;}) (':' | ';' {$used_semicolon = true;}))
+		(	('::' | (':' | s=';' {$used_semicolon = $s;}) (':' | s=';' {$used_semicolon = $s;}))
 			-> $for_exprs OMITTED
-		|	(':' | ';' {$used_semicolon = true;}) cond=any_expr (':' | ';' {$used_semicolon = true;})
+		|	(':' | s=';' {$used_semicolon = $s;}) cond=any_expr (':' | s=';' {$used_semicolon = $s;})
 			-> $for_exprs $cond
 		)
 		(	step=any_expr

File Rellor.Core/Inform6.g3.parser.cs

 
 namespace Rellor.Core
 {
-    partial class Inform6Parser
+    partial class Inform6Parser : ILogSink
     {
-        /*public bool AllowExtensions { get; set; }
-        public bool BugCompatible { get; set; }*/
+        private MsgLogger<CoreMessages> logger;
+
+        public ILogSink LogSink { get; set; }
+
+        partial void OnCreated()
+        {
+            logger = new MsgLogger<CoreMessages>(this, "RLR");
+        }
 
         private static int ParseDecInt(string str)
         {
             var thisStream = (RellorTokenStream)input;
             thisStream.GetTokens().InsertRange(thisStream.Index, includedTokens);
         }
+
+        public IGlobalScope CreateGlobalScope()
+        {
+            // TODO: initialize constants and stuff here?
+            return new GlobalScope();
+        }
+
+
+        #region ILogSink Members
+
+        void ILogSink.LogMessage(LogLevel level, string text, bool count)
+        {
+            var ls = this.LogSink;
+            if (ls != null)
+                ls.LogMessage(level, text, count);
+        }
+
+        void ILogSink.CountSuppressedMessage(LogLevel level)
+        {
+            var ls = this.LogSink;
+            if (ls != null)
+                ls.CountSuppressedMessage(level);
+        }
+
+        #endregion
     }
 }

File Rellor.Core/Messages.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+using System.ComponentModel;
+using Rellor.Core.Token;
+
+namespace Rellor.Core
+{
+    public enum LogLevel
+    {
+        /// <summary>
+        /// Internal information about the program state, useful only to Rellor's developers.
+        /// </summary>
+        Debug,
+        /// <summary>
+        /// Information about the compilation process that may be useful to game authors.
+        /// </summary>
+        Verbose,
+        /// <summary>
+        /// Benign and informative, such as the compiler banner or compilation statistics.
+        /// </summary>
+        Info,
+        /// <summary>
+        /// Cosmetic issues that probably don't indicate a programming error, such as less-preferred syntax, or
+        /// unused variables.
+        /// </summary>
+        Notice,
+        /// <summary>
+        /// Suggestions on how to address more severe messages, such as how to suppress a warning if the
+        /// suspicious meaning is really intended. Protips are automatically suppressed in certain
+        /// circumstances to avoid repeating the same tip for the same error.
+        /// </summary>
+        Protip,
+        /// <summary>
+        /// Deprecation issues, or suspicious code that likely indicates a programming error, such as
+        /// using a single '=' in a condition context.
+        /// </summary>
+        Warning,
+        /// <summary>
+        /// Illegal code that cannot be compiled, but can be skipped to continue analysis.
+        /// </summary>
+        Error,
+        /// <summary>
+        /// Code so illegal that analysis must stop immediately.
+        /// </summary>
+        Fatal,
+    }
+
+    [AttributeUsage(AttributeTargets.Field)]
+    public class MsgAttribute : Attribute
+    {
+        public MsgAttribute(int code, LogLevel level, string text)
+        {
+            Contract.Requires(code >= 0);
+            Contract.Requires(!string.IsNullOrEmpty(text));
+
+            this.Code = code;
+            this.Level = level;
+            this.Text = text;
+        }
+
+        /// <summary>
+        /// Numeric code of the message.
+        /// </summary>
+        public int Code { get; private set; }
+        /// <summary>
+        /// Severity level of the message.
+        /// </summary>
+        public LogLevel Level { get; private set; }
+        /// <summary>
+        /// Text of the message, optionally including format specifiers.
+        /// </summary>
+        public string Text { get; private set; }
+        /// <summary>
+        /// If nonzero, the message continues a previous message and should not be counted as
+        /// a separate event. It can be suppressed on its own, but will also be suppressed
+        /// if the referenced message is suppressed (transitively).
+        /// </summary>
+        public int Continues { get; set; }
+        /// <summary>
+        /// If true, the message will only be shown once.
+        /// </summary>
+        public bool OneShot { get; set; }
+        /// <summary>
+        /// If true, only the text of the message will be logged, with no code, severity level, or source location.
+        /// </summary>
+        public bool Naked { get; set; }
+    }
+
+    public enum CoreMessages
+    {
+        // 0000 general: environment, configuration, files, limits, compatibility
+        [Msg(0000, LogLevel.Fatal, "this message does not exist")]
+        ThereIsNoMessageZero,
+
+        [Msg(0001, LogLevel.Info, "Rellor version {0} targeting {1}", Naked = true)]
+        RellorBanner,
+        [Msg(0002, LogLevel.Error, "no 'Main' routine defined")]
+        NoMainRoutine,
+        [Msg(0003, LogLevel.Error, "this Inform construct is not supported at all in Rellor")]
+        NotSupported,
+        [Msg(0004, LogLevel.Error, "this Inform construct is only supported in Inform compatibility mode")]
+        CompatibilityModeRequired,
+        [Msg(0005, LogLevel.Error, "this Rellor extension is only supported in extended mode")]
+        ExtendedModeRequired,
+
+        // 0500 lexical issues
+        [Msg(0500, LogLevel.Error, "unexpected character '{0}'")]
+        UnexpectedChar,
+
+        // 1000 syntax issues
+        [Msg(1000, LogLevel.Notice, "'for' loops should use ':' instead of ';'")]
+        SemicolonUsedInFor,
+
+        // 1500 symbol usage: duplicate, undefined, unused
+        [Msg(1501, LogLevel.Error, "symbol '{0}' has already been defined as a '{1}'")]
+        SymbolAlreadyDefined,
+        [Msg(1502, LogLevel.Protip, "...previous definition is up here", Continues = 1501)]
+        PrevDefinitionHere,
+        [Msg(1503, LogLevel.Error, "symbol '{0}' is used but never defined")]
+        SymbolNotDefined,
+        [Msg(1504, LogLevel.Error, "symbol '{0}' must be defined before it is used")]
+        SymbolNotDefinedYet,
+        [Msg(1505, LogLevel.Protip, "...but the definition is down here", Continues = 1504)]
+        LateDefinitionHere,
+        [Msg(1506, LogLevel.Notice, "symbol '{0}' is defined but never used")]
+        SymbolUnused,
+        [Msg(1507, LogLevel.Error, "symbol '{0}' cannot be used in its own definition")]
+        SymbolRecursive,
+
+        // 2000 vocab/grammar: dict collisions, verb semantics, action subs
+        [Msg(2000, LogLevel.Warning, "the following dictionary word(s) are truncated to '{0}':")]
+        DictWordTruncated,
+        [Msg(2001, LogLevel.Warning, "...'{0}'", Continues = 2000)]
+        DictWordTruncatedOriginal,
+        [Msg(2002, LogLevel.Protip, "try increasing DICT_WORD_LENGTH to avoid truncation", Continues = 2000)]
+        ProtipDictWordTruncated,
+        [Msg(2003, LogLevel.Error, "action '{0}' has no matching routine '{0}Sub'")]
+        ActionSubMissing,
+        [Msg(2004, LogLevel.Protip, "it's not used by any verbs, maybe it should be a Fake_Action?", Continues = 2003)]
+        ProtipActionSubMissingFake,
+
+        // 2500 text: character sets, escape codes
+
+        // 3000 objects: object tree, inheritance, segment semantics
+
+        // 3500 directives (besides objects, text, routines)
+        [Msg(3500, LogLevel.Error, "'{0}' without matching 'If'")]
+        HeadlessIfDirective,
+        [Msg(3501, LogLevel.Error, "this '{0}' directive never terminates")]
+        RunawayIfDirective,
+        [Msg(3502, LogLevel.Error, "directive '{0}' may not be used inside a routine")]
+        DirectiveNotNestable,
+        [Msg(3503, LogLevel.Error, "directive '{0}' not supported by target '{1}'")]
+        DirectiveNotSupported,
+        [Msg(3504, LogLevel.Error, "include file '{0}' not found")]
+        IncludeNotFound,
+
+        // 4000 statements
+        [Msg(4000, LogLevel.Error, "statement '{0}' not supported by target '{1}'")]
+        StatementNotSupported,
+
+        // 4500 expressions
+        [Msg(4500, LogLevel.Error, "expression here must be a compile-time constant")]
+        ExprNotConstant,
+        [Msg(4501, LogLevel.Protip, "it isn't because it contains '{0}'", Continues = 4500)]
+        ProtipExprNotConstantBecause,
+        [Msg(4502, LogLevel.Warning, "'=' used in a condition context, did you mean '=='?")]
+        AssignmentAsCondition,
+        [Msg(4503, LogLevel.Protip, "explicitly compare the result to 0 if that's what you want to do", Continues = 4500)]
+        ProtipAssignmentAsConditionIntended,
+        [Msg(4504, LogLevel.Error, "'or' may only appear on the right-hand side of '==' or '~=', did you mean '||'?")]
+        MisplacedOrOperator,
+    }
+
+    [ContractClass(typeof(ILogSinkContract))]
+    public interface ILogSink
+    {
+        void LogMessage(LogLevel level, string text, bool count = true);
+        void CountSuppressedMessage(LogLevel level);
+    }
+
+    [ContractClassFor(typeof(ILogSink))]
+    internal abstract class ILogSinkContract : ILogSink
+    {
+        public void LogMessage(LogLevel level, string text, bool count)
+        {
+            Contract.Requires(!string.IsNullOrEmpty(text));
+        }
+
+        public void CountSuppressedMessage(LogLevel level)
+        {
+        }
+    }
+
+    public class MsgLogger<T>
+    {
+        private readonly string prefix;
+        private readonly ILogSink sink;
+        private readonly Dictionary<T, MsgAttribute> attributes = new Dictionary<T, MsgAttribute>();
+        private readonly HashSet<int> suppressed = new HashSet<int>();
+
+        // messages without source location for protip suppression...
+        private string lastNonContinuationText;
+        private readonly HashSet<string> seenProTips = new HashSet<string>();
+
+        private LogLevel minLevel = LogLevel.Info;
+
+        static MsgLogger()
+        {
+            // since there's no 'enum' generic constraint...
+            if (!typeof(T).IsEnum)
+                throw new InvalidOperationException("MsgLogger<T> must be used with an enum");
+        }
+
+        public MsgLogger(ILogSink sink, string prefix = "", LogLevel minLevel = LogLevel.Info)
+        {
+            Contract.Requires(sink != null);
+            Contract.Requires(prefix != null);
+            Contract.Ensures(this.sink == sink);
+            Contract.Ensures(this.prefix == prefix);
+
+            this.prefix = prefix;
+            this.sink = sink;
+
+            LoadAttributes();
+        }
+
+        public LogLevel MinLevel { get; set; }
+
+        private void LoadAttributes()
+        {
+            var query = from f in typeof(T).GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
+                        let key = (T)f.GetValue(null)
+                        let attrs = f.GetCustomAttributes(typeof(MsgAttribute), false).Cast<MsgAttribute>()
+                        select new { key, attrs };
+
+            foreach (var item in query)
+                foreach (var attr in item.attrs)
+                    attributes.Add(item.key, attr);
+        }
+
+        public void Log(T key, RellorToken token)
+        {
+            Contract.Requires(token != null);
+
+            var msg = attributes[key];
+            var code = msg.Code;
+            var level = msg.Level;
+
+            bool isSuppressed = false;
+
+            string textWithoutSource = null;
+
+            if (level < minLevel || suppressed.Contains(code) || (msg.Continues > 0 && suppressed.Contains(msg.Continues)))
+            {
+                isSuppressed = true;
+            }
+            else if (msg.Level == LogLevel.Protip)
+            {
+                textWithoutSource = FormatMessage(null, level, prefix, code, msg.Text);
+                string checkMsg = textWithoutSource;
+
+                if (msg.Continues > 0)
+                    checkMsg = lastNonContinuationText + checkMsg;
+
+                if (seenProTips.Contains(checkMsg))
+                    isSuppressed = true;
+                else
+                    seenProTips.Add(checkMsg);
+            }
+
+            if (isSuppressed)
+            {
+                sink.CountSuppressedMessage(level);
+                return;
+            }
+
+            if (msg.Continues == 0)
+            {
+                if (textWithoutSource == null)
+                    textWithoutSource = FormatMessage(null, level, prefix, code, msg.Text);
+
+                lastNonContinuationText = textWithoutSource;
+            }
+
+            string text = FormatMessage(token, level, prefix, code, msg.Text);
+            sink.LogMessage(level, text, true);
+
+            if (msg.OneShot)
+                suppressed.Add(code);
+        }
+
+        private static string GetLevelStr(LogLevel level)
+        {
+            switch (level)
+            {
+                case LogLevel.Debug:
+                    return "debug";
+                case LogLevel.Error:
+                    return "error";
+                case LogLevel.Fatal:
+                    return "fatal";
+                case LogLevel.Info:
+                    return "info";
+                case LogLevel.Notice:
+                    return "notice";
+                case LogLevel.Protip:
+                    return "protip";
+                case LogLevel.Verbose:
+                    return "verbose";
+                case LogLevel.Warning:
+                    return "warning";
+                default:
+                    throw new ArgumentException("invalid level");
+            }
+        }
+
+        private string FormatMessage(RellorToken token, LogLevel level, string prefix, int code, string text)
+        {
+            Contract.Requires(prefix != null);
+            Contract.Requires(!string.IsNullOrEmpty(text));
+            Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
+
+            if (token == null)
+                return string.Format("{0} {1}{2}: {3}", GetLevelStr(level), prefix, code, text);
+            else
+                return string.Format("{0}:{1}:{2}: {3} {4}{5}: {6}",
+                    token.InputStream.SourceName, token.Line, token.CharPositionInLine,
+                    GetLevelStr(level), prefix, code, text);
+        }
+    }
+}

File Rellor.Core/Rellor.Core.csproj

     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Analyzer.cs" />
+    <Compile Include="ITargetCapabilities.cs" />
+    <Compile Include="ITargetValidation.cs" />
+    <Compile Include="Messages.cs" />
+    <Compile Include="RellorContracts.cs" />
     <Compile Include="Token\RellorToken.cs" />
     <Compile Include="Token\RellorTokenStream.cs" />
     <Compile Include="Tree\NumberTree.cs" />
     <Compile Include="SpecializeOperators.g3.cs">
       <DependentUpon>SpecializeOperators.g3</DependentUpon>
     </Compile>
+    <Compile Include="Vocab\GrammarLine.cs" />
+    <Compile Include="Vocab\Verb.cs" />
+    <Compile Include="Vocab\Word.cs" />
   </ItemGroup>
   <ItemGroup>
     <Antlr3 Include="Inform6.g3">

File Rellor.Core/RellorContracts.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+
+namespace Rellor.Core
+{
+    public static class RellorContracts
+    {
+        [Pure]
+        public static bool SequencesAreIdentical<T>(IEnumerable<T> first, IEnumerable<T> second)
+            where T : class
+        {
+            Contract.Requires(first != null);
+            Contract.Requires(second != null);
+
+            var tor1 = first.GetEnumerator();
+            var tor2 = second.GetEnumerator();
+
+            while (true)
+            {
+                bool more1 = tor1.MoveNext();
+                bool more2 = tor2.MoveNext();
+
+                if (more1 != more2)
+                    return false;
+
+                if (!more1)
+                    return true;
+
+                if (tor1.Current != tor2.Current)
+                    return false;
+            }
+        }
+
+        [Pure]
+        public static int IndexOf<T>(T[] array, T value)
+        {
+            return Array.IndexOf(array, value);
+        }
+    }
+}

File Rellor.Core/Scope.cs

         }
     }
 
-    class GlobalScope : ScopeBase
+    [ContractClass(typeof(IGlobalScopeContract))]
+    public interface IGlobalScope : IScope
+    {
+        ushort Release { get; set; }
+        string Serial { get; set; }
+    }
+
+    [ContractClassFor(typeof(IGlobalScope))]
+    internal abstract class IGlobalScopeContract : IGlobalScope
+    {
+        public ushort Release { get; set; }
+        public string Serial { get; set; }
+
+        [ContractInvariantMethod]
+        private void ObjectInvariant()
+        {
+            Contract.Invariant(Serial != null && Serial.Length == 6);
+        }
+
+        #region IScope Members
+        public abstract string Name { get; }
+        public abstract Symbol Resolve(string name);
+        public abstract void Define(string name, Symbol symbol);
+        #endregion
+    }
+
+    class GlobalScope : ScopeBase, IGlobalScope
     {
         public GlobalScope()
             : base(null)
         {
+            Serial = DateTime.Now.ToString("yyMMdd");
         }
 
         public override string Name
         {
             get { return "<global>"; }
         }
+
+        public ushort Release { get; set; }
+        public string Serial { get; set; }
     }
 
     class RoutineScope : ScopeBase

File Rellor.Core/Tree/StringTree.cs

-using System.Diagnostics.Contracts;
-using Antlr.Runtime;
-using Antlr.Runtime.Tree;
-using Rellor.Core.Token;
-
-namespace Rellor.Core.Tree
-{
-    public class StringTree : CommonTree
-    {
-        public string String { get; private set; }
-
-        public StringTree(int ttype, string str)
-            : this(ttype, new RellorToken(ttype), str) { }
-
-        public StringTree(int ttype, IToken token, string str)
-            : base(token.Type == ttype ? token : new RellorToken(token) { Type = ttype })
-        {
-            Contract.Requires(token != null);
-            Contract.Requires(str != null);
-
-            this.String = str;
-        }
-    }
-}

File Rellor.Core/Vocab/GrammarLine.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+
+namespace Rellor.Core.Vocab
+{
+    public abstract class GrammarToken
+    {
+        protected GrammarToken(int type)
+        {
+            ValidateTokenType(type);
+            this.Type = type;
+        }
+
+        private static void ValidateTokenType(int type)
+        {
+            switch (type)
+            {
+                case Inform6Parser.HELD:
+                    // OK
+                    break;
+
+                default:
+                    throw new ArgumentException("Unexpected grammar token type: " + type);
+            }
+        }
+
+        public int Type { get; private set; }
+    }
+
+    public class SimpleGrammarToken : GrammarToken
+    {
+        public SimpleGrammarToken(int type)
+            : base(type) { }
+    }
+
+    public class RoutineGrammarToken : GrammarToken
+    {
+        public RoutineGrammarToken(int type, string routineName)
+            : base(type)
+        {
+            this.RoutineName = routineName;
+        }
+
+        public string RoutineName { get; private set; }
+    }
+
+    public class GrammarLine
+    {
+        private readonly GrammarToken[] tokens;
+        private readonly string action;
+
+        public GrammarLine(GrammarToken[] tokens, string action)
+        {
+            Contract.Requires(tokens != null && tokens.Length >= 1);
+            Contract.Requires(!string.IsNullOrEmpty(action));
+
+            this.tokens = tokens;
+            this.action = action;
+        }
+
+        public IEnumerable<GrammarToken> Tokens
+        {
+            get { return tokens; }
+        }
+
+        public string Action
+        {
+            get { return action; }
+        }
+    }
+}

File Rellor.Core/Vocab/Verb.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+
+namespace Rellor.Core.Vocab
+{
+    public class Verb
+    {
+        private readonly Word[] words;
+        private readonly List<GrammarLine> grammarLines = new List<GrammarLine>();
+        private readonly bool isMeta;
+
+        public Verb(Word[] words, bool isMeta)
+        {
+            Contract.Requires(words != null && words.Length >= 1);
+            Contract.Requires(Contract.ForAll(0, words.Length - 1, i => RellorContracts.IndexOf(words, words[i]) == i));  // no duplicates
+            Contract.Ensures(this.words.Length == words.Length);
+            Contract.Ensures(Contract.ForAll(0, words.Length - 1, i => this.words[i] == words[i]));
+            Contract.Ensures(grammarLines.Count == 0);
+            Contract.Ensures(this.isMeta == isMeta);
+
+            this.words = (Word[])words.Clone();
+            this.isMeta = isMeta;
+        }
+
+        public IEnumerable<Word> Words
+        {
+            get
+            {
+                Contract.Ensures(Contract.Result<IEnumerable<Word>>().Count() == this.words.Length);
+                return words;
+            }
+        }
+
+        public IList<GrammarLine> GrammarLines
+        {
+            get
+            {
+                Contract.Ensures(Contract.Result<IList<GrammarLine>>() == this.grammarLines);
+                return grammarLines;
+            }
+        }
+
+        public bool IsMeta
+        {
+            get
+            {
+                Contract.Ensures(Contract.Result<bool>() == this.isMeta);
+                return isMeta;
+            }
+        }
+
+        public void Split(Word[] wordsToKeep, out Verb kept, out Verb other)
+        {
+            Contract.Requires(wordsToKeep != null && wordsToKeep.Length > 0);
+            // either kept or other or both will be returned
+            Contract.Ensures(Contract.ValueAtReturn<Verb>(out kept) != null || Contract.ValueAtReturn<Verb>(out other) != null);
+            // if kept is returned, it will have the same words as wordsToKeep
+            //Contract.Ensures(Contract.ValueAtReturn<Verb>(out kept) == null || RellorContracts.SequencesAreIdentical(Contract.ValueAtReturn<Verb>(out kept).Words, wordsToKeep));
+            // new verbs have the same meta flag
+            Contract.Ensures(Contract.ValueAtReturn<Verb>(out kept) == null || Contract.ValueAtReturn<Verb>(out kept).isMeta == this.isMeta);
+            Contract.Ensures(Contract.ValueAtReturn<Verb>(out other) == null || Contract.ValueAtReturn<Verb>(out other).isMeta == this.isMeta);
+
+            var otherWords = (from w in this.words
+                              where Array.IndexOf(wordsToKeep, w) == -1
+                              select w).ToArray();
+
+            if (otherWords.Length == 0)
+            {
+                kept = this;
+                other = null;
+            }
+            else if (otherWords.Length == this.words.Length)
+            {
+                kept = null;
+                other = this;
+            }
+            else
+            {
+                kept = new Verb(wordsToKeep, this.isMeta);
+                other = new Verb(otherWords, this.isMeta);
+
+                kept.grammarLines.AddRange(this.grammarLines);
+                other.grammarLines.AddRange(this.grammarLines);
+            }
+        }
+    }
+}

File Rellor.Core/Vocab/Word.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Rellor.Core.Vocab
+{
+    public class Word
+    {
+    }
+}

File RellorC/ConsoleLogSink.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Rellor.Core;
+
+namespace RellorC
+{
+    class ConsoleLogSink : ILogSink
+    {
+        public void LogMessage(LogLevel level, string text, bool count = true)
+        {
+            Console.WriteLine(text);
+        }
+
+        public void CountSuppressedMessage(LogLevel level)
+        {
+            // nada
+        }
+    }
+}

File RellorC/Program.cs

         static void Main(string[] args)
         {
             const string SText =
-@"
+@"[ Main; for(;;) print ""RELLOR""; ];";
+/*@"
+#include ""Global blah = 1;"";
 Array foo -> 1 2 3;
 Array bar --> 5;
 Array baz --> (5);
 [ Main;
     x == 1 or 2 or 3;
+    #include ""x++;"";
     !while (x < $+1.5e6) { @""fadd"" x $+5 -> sp ?~foo; }
     !do { j++, k++; } until (j ~= k or 5 or 3 or 4+1 or 0);
-];";
+];";*/
 /*@"
 [ Main;
     switch (foo) {
   has metal ~porcelain,
   with before [; print ""Don't touch that.^""; ];
 ";*/
-
-            Inform6Lexer lexer = new Inform6Lexer(new ANTLRStringStream(SText));
-            Inform6Parser parser = new Inform6Parser(new RellorTokenStream(lexer));
-            var parserResult = parser.program();
+            var logSink = new ConsoleLogSink();
+            var lexer = new Inform6Lexer(new ANTLRStringStream(SText) { name = "<string>" });
+            var parser = new Inform6Parser(new RellorTokenStream(lexer)) { LogSink = logSink };
+            var parserResult = parser.program(null);
 
             Console.WriteLine("Parser result:");
             DumpTree(parserResult.Tree, parser.TokenNames);
                     case Inform6Parser.SQ_STRING:
                     case Inform6Parser.DQ_STRING:
                     case Inform6Parser.HASHDOLLAR:
+                    case Inform6Parser.BEGIN_INCLUDE:
+                    case Inform6Parser.END_INCLUDE:
                         Console.Write(' ');
                         Console.Write(tree.Text);
                         break;

File RellorC/RellorC.csproj

     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="ConsoleLogSink.cs" />
     <Compile Include="Program.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>