Read about our upcoming Code of Conduct on this issue

This instance was upgraded to Heptapod 0.28.0 today

Program.cs 70.3 KB
Newer Older
1
/* Copyright 2010-2018 Jesse McGrew
2
 * 
Jesse McGrew's avatar
Jesse McGrew committed
3
 * This file is part of ZILF.
4
 * 
Jesse McGrew's avatar
Jesse McGrew committed
5
 * ZILF is free software: you can redistribute it and/or modify it
6
7
8
9
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
Jesse McGrew's avatar
Jesse McGrew committed
10
 * ZILF is distributed in the hope that it will be useful, but
11
12
13
14
15
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
Jesse McGrew's avatar
Jesse McGrew committed
16
 * along with ZILF.  If not, see <http://www.gnu.org/licenses/>.
17
18
19
20
 */

using System;
using System.Collections.Generic;
21
using System.Diagnostics;
Jesse McGrew's avatar
Jesse McGrew committed
22
using System.IO;
23
24
25
26
using System.Linq;
using System.Reflection;
using System.Text;
using Zapf.Parsing;
27
using Zilf.Common.StringEncoding;
28
29
30
31
using Zapf.Parsing.Diagnostics;
using Zapf.Parsing.Directives;
using Zapf.Parsing.Expressions;
using Zapf.Parsing.Instructions;
32
using System.Diagnostics.CodeAnalysis;
33
34
35

namespace Zapf
{
36
    static class Program
37
38
39
40
41
    {
        // TODO: Blorb output

        public const byte DEFAULT_ZVERSION = 3;

42
        public static int Main(string[] args)
43
        {
44
            if (!TryParseArgs(args, out var ctx))
45
46
47
48
49
50
51
            {
                Usage();
                return 1;
            }

            // show banner
            if (!ctx.Quiet)
52
                Console.Error.WriteLine(GetBanner());
53

54
55
            // TODO: move all of this logic into ZapfAssembler and use that instead

56
57
            // set up for the target version
            ctx.OpcodeDict = MakeOpcodeDict(ctx.ZVersion, ctx.InformMode);
58
            ctx.Restart();
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

            try
            {
                // assemble the code
                bool restart;
                do
                {
                    restart = false;
                    try
                    {
                        Assemble(ctx);
                    }
                    catch (RestartException)
                    {
                        if (!ctx.Quiet)
                            Console.Error.WriteLine("\nRestarting");

76
                        ctx.Restart();
77
78
79
80
81
82
83
84
85
86
                        restart = true;
                    }
                    catch (FatalError fer)
                    {
                        ctx.HandleFatalError(fer);
                        return 2;
                    }
                } while (restart);

                // list label addresses
87
                PrintLabelAddresses(ctx);
88
89

                // find abbreviations
90
                FindAndPrintAbbreviations(ctx);
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

                // report success or failure
                if (ctx.ErrorCount > 0)
                {
                    if (!ctx.Quiet)
                    {
                        Console.Error.WriteLine();
                        Console.Error.WriteLine("Failed ({0} error{1})",
                            ctx.ErrorCount,
                            ctx.ErrorCount == 1 ? "" : "s");
                    }
                    return 2;
                }

                return 0;
            }
            finally
            {
                ctx.CloseOutput();
                ctx.CloseDebugFile();
            }
        }

114
115
        internal static string GetVersion() =>
            typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
116
                ?.InformationalVersion ?? "<?.??>";
117
118
119

        internal static string GetBanner() => $"ZAPF {GetVersion()}";

120
        static void FindAndPrintAbbreviations(Context ctx)
121
122
123
124
125
126
127
128
129
        {
            if (ctx.AbbreviateMode)
            {
                const int maxAbbrevs = 96;
                Console.Error.WriteLine("Finding up to {0} abbreviations...", maxAbbrevs);

                Console.WriteLine("        ; Frequent words file for {0}", Path.GetFileName(ctx.InFile));
                Console.WriteLine();
                int num = 1, totalSavings = 0;
Jesse McGrew's avatar
Jesse McGrew committed
130
                foreach (var r in ctx.AbbrevFinder.GetResults(maxAbbrevs))
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
                {
                    Console.WriteLine("        .FSTR FSTR?{0},\"{1}\"\t\t; {2}x, saved {3}",
                        num++, SanitizeString(r.Text), r.Count, r.Score);
                    totalSavings += r.Score;
                }

                if (num < maxAbbrevs)
                    Console.WriteLine("        .FSTR FSTR?DUMMY,\"\"");
                Console.WriteLine("WORDS::");
                for (int i = 1; i < num; i++)
                    Console.WriteLine("        FSTR?{0}", i);
                for (int i = num; i < maxAbbrevs; i++)
                    Console.WriteLine("        FSTR?DUMMY");

                Console.WriteLine();
                Console.WriteLine("        .ENDI");

                Console.Error.WriteLine("Abbrevs would save {0} z-chars total (~{1} bytes)",
                    totalSavings, totalSavings * 2 / 3);
            }
        }

153
        static void PrintLabelAddresses(Context ctx)
154
155
156
157
158
159
        {
            if (ctx.ListAddresses)
            {
                Console.Error.WriteLine();

                var query = from s in ctx.GlobalSymbols.Values
160
161
162
163
164
165
166
167
168
169
                            let addr = s.Type switch
                            {
                                SymbolType.Label => s.Value,
                                SymbolType.Function => s.Value * ctx.PackingDivisor + ctx.FunctionsOffset,
                                SymbolType.String => s.Value * ctx.PackingDivisor + ctx.StringsOffset,
                                _ => (int?)null
                            }
                            where addr != null
                            orderby addr.Value
                            select new { s.Name, Address = addr.Value };
170
171
172
173
174
175

                Console.WriteLine("{0,-32} {1,-6} {2,-6}",
                    "Name",
                    "Addr",
                    "Length");

176
                // TODO: don't allocate an array for the label addresses
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
                var entries = query.ToArray();

                for (int i = 0; i < entries.Length; i++)
                {
                    var entry = entries[i];

                    object dist;
                    if (i < entries.Length - 1)
                        dist = entries[i + 1].Address - entries[i].Address;
                    else
                        dist = "to end";

                    Console.WriteLine("{0,-32} ${1:x5} {2,6}",
                        entry.Name,
                        entry.Address,
                        dist);
                }
            }
        }

197
        static string SanitizeString(string text)
198
        {
199
            var sb = new StringBuilder(text);
200
201
202
203
204
205
206
207

            for (int i = sb.Length - 1; i >= 0; i--)
                if (sb[i] == '"')
                    sb.Insert(i, '"');

            return sb.ToString();
        }

208
        internal static Dictionary<string, KeyValuePair<ushort, ZOpAttribute>> MakeOpcodeDict(
209
210
            int zversion, bool inform)
        {
211
            var fields = typeof(Opcodes).GetFields(BindingFlags.Static | BindingFlags.Public);
212
213
214
215
            var result = new Dictionary<string, KeyValuePair<ushort, ZOpAttribute>>(fields.Length);

            int effectiveVersion = zversion;

Jesse McGrew's avatar
Jesse McGrew committed
216
            if (zversion == 7 || zversion == 8)
217
218
                effectiveVersion = 5;

Jesse McGrew's avatar
Jesse McGrew committed
219
            foreach (var fi in fields)
220
            {
221
                var attrs = fi.GetCustomAttributes(typeof(ZOpAttribute), false);
222
223
224
                foreach (ZOpAttribute attr in attrs)
                    if (effectiveVersion >= attr.MinVer && effectiveVersion <= attr.MaxVer)
                    {
225
                        var pair = new KeyValuePair<ushort, ZOpAttribute>((ushort)fi.GetValue(null)!, attr);
226
227
228
229
230
231
232
233
                        result.Add(inform ? attr.InformName : attr.ClassicName, pair);
                        break;
                    }
            }

            return result;
        }

234
        internal static bool TryParseArgs(IReadOnlyList<string> args, [NotNullWhen(true)] out Context? ctx)
235
        {
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
            var result = new Context();

            if (TryParseArgs(args, result))
            {
                ctx = result;
                return true;
            }

            ctx = null;
            return false;
        }

        internal static bool TryParseArgs(IReadOnlyList<string> args, Context ctx)
        {
            string? inFile = null, outFile = null;
251

252
            for (int i = 0; i < args.Count; i++)
253
254
255
256
            {
                switch (args[i])
                {
                    case "-ab":
257
                        ctx.AbbreviateMode = true;
258
259
260
                        break;

                    case "-q":
261
                        ctx.Quiet = true;
262
263
264
                        break;

                    case "-i":
265
                        ctx.InformMode = true;
266
267
268
                        break;

                    case "-la":
269
                        ctx.ListAddresses = true;
270
271
272
                        break;

                    case "-r":
273
                        if (++i == args.Count)
274
275
                            return false;
                        ctx.Release = short.Parse(args[i]);
276
277
278
                        break;

                    case "-s":
279
                        if (++i == args.Count)
280
281
                            return false;
                        ctx.Serial = args[i];
282
283
284
                        break;

                    case "-c":
285
                        if (++i == args.Count)
286
287
                            return false;
                        ctx.Creator = args[i];
288
289
290
                        break;

                    case "-c0":
291
                        ctx.Creator = null;
292
293
                        break;

Jesse McGrew's avatar
Jesse McGrew committed
294
                    case "-dx":
295
                        ctx.XmlDebugMode = true;
Jesse McGrew's avatar
Jesse McGrew committed
296
297
                        break;

298
299
300
                    case "-?":
                    case "--help":
                    case "/?":
301
                        return false;
302
303

                    default:
304
305
306
307
                        if (inFile == null)
                            inFile = args[i];
                        else if (outFile == null)
                            outFile = args[i];
308
                        else
309
                            return false;
310
311
312
313
314
                        break;
                }
            }

            // validate
315
316
            if (inFile == null)
                return false;
317

318
319
320
            ctx.InFile = inFile;
            ctx.OutFile = outFile ?? Path.ChangeExtension(ctx.InFile, ".z#");
            ctx.DebugFile = Path.ChangeExtension(ctx.OutFile, ctx.XmlDebugMode ? ".dbg.xml" : ".dbg");
321

322
            return true;
323
324
        }

325
        static void Usage()
326
        {
327
            Console.Error.WriteLine(GetBanner());
328
329
330
331
            Console.Error.WriteLine(
@"Assemble: zapf [switches] <inFile.zap> [<outFile.z#>]

General switches:
332
  -i                    use Inform opcode/stack names
333
334
  -q                    quiet (no banner)
  -la                   list global label addresses
335
  -r #                  set release number (this overrides RELEASEID)
336
337
  -s ######             set serial number
  -c ####               set creator version
Jesse McGrew's avatar
Jesse McGrew committed
338
339
  -ab                   also optimize abbreviations and print ZAPF code
  -dx                   use XML debug format");
340
341
        }

342
        /// <exception cref="FatalError">An <see cref="IOException"/> occurred while reading the input file(s).</exception>
343
        internal static void Assemble(Context ctx)
344
        {
345
            List<AsmLine> file;
346
            Debug.Assert(ctx.InFile != null);
347
348
349
            ctx.PushFile(ctx.InFile);
            try
            {
350
351
                var roots = ReadRootsFromFile(ctx, ctx.InFile);
                file = new List<AsmLine>(ReadAllCode(ctx, roots));
352
353
354
355
            }
            catch (IOException ex)
            {
                Errors.ThrowFatal(ex.Message);
356
                // ReSharper disable once HeuristicUnreachableCode
357
358
359
360
361
                return; // never gets here
            }

            // first pass: discover label addresses and header flags
            if (!ctx.Quiet)
362
                Console.Error.Write("Measuring");
363

364
            ctx.FinalPass = false;
365

366
367
368
            do
            {
                ctx.MeasureAgain = false;
369

370
371
372
373
374
375
                // write dummy header
                ctx.Position = 0;
                if (ctx.ZVersion < 5)
                    WriteHeader(ctx, false);

                for (int i = 0; i < file.Count; i++)
376
                {
377
378
                    try
                    {
379
                        if (file[i] is EndDirective)
380
                            break;
381
382

                        PassOne(ctx, file[i], ref i);
383
384
385
386
387
                    }
                    catch (SeriousError ser)
                    {
                        ctx.HandleSeriousError(ser);
                    }
388
                }
389

390
391
                ctx.CheckForUndefinedSymbols();

392
393
394
                if (ctx.Fixups.Count > 0)
                    ctx.MeasureAgain = true;

395
                ctx.CheckLimits();
396
397
398
399
400
401
402
403
                ctx.ResetBetweenPasses();

                if (!ctx.Quiet && ctx.MeasureAgain && ctx.ErrorCount == 0)
                    Console.Error.Write('.');
            } while (ctx.MeasureAgain && ctx.ErrorCount == 0);

            if (!ctx.Quiet)
                Console.Error.WriteLine();
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421

            if (ctx.ErrorCount == 0)
            {
                // open output file and write mostly-real header
                ctx.OpenOutput();
                if (ctx.ZVersion < 5)
                {
                    try
                    {
                        WriteHeader(ctx, true);
                    }
                    catch (SeriousError ser)
                    {
                        ctx.HandleSeriousError(ser);
                    }
                }
            }

422
423
424
            // verify packed address labels
            foreach (var sym in ctx.GlobalSymbols.Values)
            {
425
426
427
                if (sym.Value < 65536)
                    continue;

428
429
                Debug.Assert(sym.Name != null);

430
                switch (sym.Type)
431
                {
432
433
434
                    case SymbolType.Function:
                        Errors.Warn(ctx, null, "packed address overflow for function: {0}", sym.Name);
                        break;
435

436
437
438
                    case SymbolType.String:
                        Errors.Warn(ctx, null, "packed address overflow for string: {0}", sym.Name);
                        break;
439
440
441
                }
            }

442
443
444
445
446
447
448
449
            // stop early if errors detected
            if (ctx.ErrorCount > 0)
                return;

            // second pass: generate object code
            if (!ctx.Quiet)
                Console.Error.WriteLine("Assembling");

450
            ctx.FinalPass = true;
451

452
            for (int i = 0; i < file.Count; i++)
453
            {
454
455
                try
                {
456
                    if (file[i] is EndDirective)
457
                        break;
458
459

                    PassTwo(ctx, file[i], ref i);
460
461
462
463
464
                }
                catch (SeriousError ser)
                {
                    ctx.HandleSeriousError(ser);
                }
465
            }
466
467
468

            if (ctx.Fixups.Count > 0)
                Errors.Serious(ctx, "unresolved references after final pass");
469

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
            // pad if necessary, finalize header length and checksum, close file
            try
            {
                FinalizeOutput(ctx);
            }
            catch (SeriousError ser)
            {
                ctx.HandleSeriousError(ser);
            }
        }

        /// <summary>
        /// Apply the first assembler pass to a node.
        /// </summary>
        /// <remarks>
        /// This pass discovers label addresses and header flags.
        /// It also handles the .NEW directive.
        /// </remarks>
        /// <param name="ctx">The current context.</param>
        /// <param name="node">The node to process.</param>
490
491
        /// <param name="nodeIndex">The current node's index. The method may change this
        /// to rewind the source file.</param>
492
        static void PassOne(Context ctx, AsmLine node, ref int nodeIndex)
493
        {
Jesse McGrew's avatar
Jesse McGrew committed
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
            switch (node)
            {
                case NewDirective newd:
                    var versionExpr = newd.Version;
                    int version = versionExpr == null ? 4 : EvalExpr(ctx, versionExpr).Value;
                    if (version < 3 || version > 8)
                        Errors.ThrowFatal("Only Z-machine versions 3-8 are supported");
                    if (version != ctx.ZVersion)
                    {
                        ctx.ZVersion = (byte)version;
                        ctx.OpcodeDict = MakeOpcodeDict(ctx.ZVersion, ctx.InformMode);
                        throw new RestartException();
                    }
                    break;

                case TimeDirective _:
                    if (ctx.ZVersion == 3)
                    {
                        ctx.ZFlags |= 2;
                    }
                    else
                    {
                        Errors.ThrowFatal(".TIME is only supported in Z-machine version 3");
                    }
                    break;

                case SoundDirective _:
521
                    switch (ctx.ZVersion)
Jesse McGrew's avatar
Jesse McGrew committed
522
                    {
523
524
525
526
527
528
529
530
531
                        case 3:
                            ctx.ZFlags2 |= 16;
                            break;
                        case 4:
                            ctx.ZFlags2 |= 128;
                            break;
                        default:
                            Errors.ThrowFatal(".SOUND is only supported in Z-machine versions 3-4");
                            break;
Jesse McGrew's avatar
Jesse McGrew committed
532
533
534
535
                    }
                    break;

                case Instruction inst:
536
                    HandleInstruction(ctx, inst);
Jesse McGrew's avatar
Jesse McGrew committed
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
                    break;

                case BareSymbolLine bsl:
                    if (bsl.UsedAsInstruction)
                        Errors.Serious(ctx, node, "unrecognized opcode: " + bsl.Text);
                    else
                        HandleDirective(ctx, node, nodeIndex, false);
                    break;

                case LocalLabel _:
                case GlobalLabel _:
                    HandleLabel(ctx, node, ref nodeIndex);
                    break;

                default:
552
                    HandleDirective(ctx, node, nodeIndex, false);
Jesse McGrew's avatar
Jesse McGrew committed
553
                    break;
554
555
556
            }
        }

557
        static void WriteHeader(Context ctx, bool strict)
558
559
560
561
        {
            ctx.WriteByte(ctx.ZVersion);
            ctx.WriteByte(ctx.ZFlags);
            // release number
562
            ctx.WriteWord((ushort)ctx.GetHeaderValue("RELEASEID", "ZORKID", false));
563
            // high memory mark (ENDLOD)
564
            var endlod = ctx.GetHeaderValue("ENDLOD", strict);
565
566
            ctx.WriteWord((ushort)endlod);
            // initial program counter (START)
567
            var start = ctx.GetHeaderValue("START", strict);
568
569
            ctx.WriteWord((ushort)start);
            // pointer to dictionary (VOCAB)
570
            ctx.WriteWord((ushort)ctx.GetHeaderValue("VOCAB", strict));
571
            // pointer to objects (OBJECT)
572
            ctx.WriteWord((ushort)ctx.GetHeaderValue("OBJECT", strict));
573
            // pointer to global variables (GLOBAL)
574
            ctx.WriteWord((ushort)ctx.GetHeaderValue("GLOBAL", strict));
575
            // static memory mark (IMPURE - the memory *below* this point is impure)
576
            var impure = ctx.GetHeaderValue("IMPURE", strict);
577
578
            ctx.WriteWord((ushort)impure);
            // flags 2 word
579
            ctx.WriteWord(ctx.ZFlags2);
580
581
582
583
584
            // serial number (6 bytes)
            ctx.WriteWord(0);   // filled in later
            ctx.WriteWord(0);
            ctx.WriteWord(0);
            // pointer to abbreviations (WORDS)
585
            ctx.WriteWord((ushort)ctx.GetHeaderValue("WORDS", strict));
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
            // packed program length
            ctx.WriteWord(0);   // we'll fill this in later
            // checksum
            ctx.WriteWord(0);   // this too

            while (ctx.Position < 64)
                ctx.WriteByte(0);

            // validate header fields
            if (start > 65536)
                Errors.ThrowSerious("START must be in the first 64k (currently {0})", start);
            if (impure > 65536)
                Errors.ThrowSerious("IMPURE must be in the first 64k (currently {0})", impure);
            if (endlod < impure)
                Errors.ThrowSerious("ENDLOD must be after IMPURE");
        }

603
        static Symbol? GetDebugMapValue(Context ctx, string name)
604
        {
Jesse McGrew's avatar
Jesse McGrew committed
605
606
            ctx.GlobalSymbols.TryGetValue(name, out var sym);
            return sym;
607
608
        }

609
        static void FinalizeOutput(Context ctx)
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
        {
            const int MINSIZE = 512;

            // pad file to a minimum size (for the benefit of tools that assume one)
            while (ctx.Position < MINSIZE)
                ctx.WriteByte(0);

            // pad file to a nice round number
            while (ctx.Position % ctx.HeaderLengthDivisor != 0)
                ctx.WriteByte(0);

            // calculate file length
            int length = ctx.Position;

            int maxLength = 0;
            switch (ctx.ZVersion)
            {
                case 1:
                case 2:
                case 3:
                    maxLength = 128;
                    break;
                case 4:
                case 5:
                    maxLength = 256;
                    break;
                case 7:
                    maxLength = 320;
                    break;
                case 6:
                case 8:
                    maxLength = 512;
                    break;
            }

            if (length > maxLength * 1024)
646
647
648
649
650
651
652
653
654
655
656
657
658
659
            {
                string protip;
                if (ctx.StringEncoder.AbbreviationCount < 96)
                {
                    protip = " (generating text abbreviations may help; see documentation for the -ab switch)";
                }
                else
                {
                    protip = "";
                }

                Errors.ThrowSerious("file length of {0} exceeds platform limit by {1} bytes{2}",
                    length, length - maxLength * 1024, protip);
            }
660
661
662
663
664
665
666
667
668
669
670
671

            // calculate file checksum
            ctx.Position = 64;
            ushort checksum = 0;

            while (ctx.Position < length)
                checksum += ctx.ReadByte();

            // write Z-code version into header
            ctx.Position = 0;
            ctx.WriteByte(ctx.ZVersion);

672
673
674
675
676
677
678
            // write release number into header (overriding RELEASEID if set)
            if (ctx.Release != null)
            {
                ctx.Position = 2;
                ctx.WriteWord((ushort)ctx.Release);
            }

679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
            // write serial number into header
            if (ctx.Serial == null)
                ctx.Serial = DateTime.Now.ToString("yyMMdd");
            else if (ctx.Serial.Length != 6)
                ctx.Serial = ctx.Serial.PadRight(6).Substring(0, 6);

            ctx.Position = 0x12;
            foreach (char c in ctx.Serial)
                ctx.WriteByte((byte)c);

            // write length and checksum into header
            ctx.Position = 0x1A;
            ctx.WriteWord((ushort)(length / ctx.HeaderLengthDivisor));
            ctx.WriteWord(checksum);

            // write creator ID
            if (ctx.Creator != null)
            {
                if (ctx.Creator.Length != 4)
                    ctx.Creator = ctx.Creator.PadRight(4).Substring(0, 4);

                ctx.Position = 0x3C;
                foreach (char c in ctx.Creator)
                    ctx.WriteByte((byte)c);
            }

            // done
            if (!ctx.Quiet)
                Console.Error.WriteLine("Wrote {0} bytes to {1}", length, ctx.OutFile);

            // finalize debug file
            if (ctx.IsDebugFileOpen)
            {
712
713
                Debug.Assert(ctx.DebugWriter != null);

714
                // finish map
715
716
717
718
719
720
721
722
723
724
725
                void CopyDebugMapValue(string debfName, string symbolName)
                {
                    if (!ctx.DebugFileMap.ContainsKey(debfName) && GetDebugMapValue(ctx, symbolName) is Symbol sym)
                        ctx.DebugFileMap[debfName] = sym;
                }

                CopyDebugMapValue(DEBF.AbbrevMapName, "WORDS");
                CopyDebugMapValue(DEBF.GlobalsMapName, "GLOBAL");
                CopyDebugMapValue(DEBF.PropsMapName, "OBJECT");
                CopyDebugMapValue(DEBF.VocabMapName, "VOCAB");

726
727
                if (!ctx.DebugFileMap.ContainsKey(DEBF.ObjectsMapName))
                {
728
                    var objTable = GetDebugMapValue(ctx, "OBJECT");
729
730
                    if (objTable != null)
                    {
731
                        ctx.DebugFileMap[DEBF.ObjectsMapName] = new Symbol
732
733
734
735
                        {
                            Type = objTable.Type,
                            Value = objTable.Value + (ctx.ZVersion < 4 ? 31 : 63)
                        };
736
737
738
739
                    }
                }

                // write map
740
741
742
                ctx.DebugWriter.WriteMap(
                    ctx.DebugFileMap.Select(
                        p => new KeyValuePair<string, int>(p.Key, p.Value.Value)));
743
744

                // write header
745
                ctx.DebugWriter.WriteHeader(ctx.GetHeader());
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764

                // done
                ctx.CloseDebugFile();

                if (!ctx.Quiet)
                    Console.Error.WriteLine("Wrote debugging info to {0}", ctx.DebugFile);
            }

            ctx.CloseOutput();
        }

        /// <summary>
        /// Apply the second assembler pass to a node.
        /// </summary>
        /// <remarks>
        /// This pass generates code.
        /// </remarks>
        /// <param name="ctx">The current context.</param>
        /// <param name="node">The node to process.</param>
765
766
        /// <param name="nodeIndex">The current node's index. The method may change this
        /// to rewind the source file.</param>
767
        static void PassTwo(Context ctx, AsmLine node, ref int nodeIndex)
768
        {
Jesse McGrew's avatar
Jesse McGrew committed
769
            switch (node)
770
            {
Jesse McGrew's avatar
Jesse McGrew committed
771
                case Instruction inst:
772
                    HandleInstruction(ctx, inst);
Jesse McGrew's avatar
Jesse McGrew committed
773
774
775
776
777
778
779
780
781
782
                    break;

                case LocalLabel _:
                case GlobalLabel _:
                    HandleLabel(ctx, node, ref nodeIndex);
                    break;

                default:
                    HandleDirective(ctx, node, nodeIndex, true);
                    break;
783
784
785
            }
        }

786
        static IEnumerable<AsmLine> ReadAllCode(Context ctx, IEnumerable<AsmLine> roots)
787
        {
788
            foreach (var node in roots)
789
            {
Jesse McGrew's avatar
Jesse McGrew committed
790
                if (node is InsertDirective insert)
791
                {
Jesse McGrew's avatar
Jesse McGrew committed
792
                    string arg = insert.InsertFileName;
793
                    var insertedFile = ctx.FindInsertedFile(arg);
794
795

                    if (insertedFile == null)
796
                        Errors.ThrowFatal(node, "inserted file not found: {0}", arg);
797
798
799
800

                    ctx.PushFile(insertedFile);
                    try
                    {
801
                        var insertedRoots = ReadRootsFromFile(ctx, insertedFile);
Jesse McGrew's avatar
Jesse McGrew committed
802
                        foreach (var insertedNode in ReadAllCode(ctx, insertedRoots))
803
                            if (insertedNode is EndiDirective || insertedNode is EndDirective)
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
                                break;
                            else
                                yield return insertedNode;
                    }
                    finally
                    {
                        ctx.PopFile();
                    }
                }
                else
                {
                    yield return node;
                }
            }
        }

820
        static IEnumerable<AsmLine> ReadRootsFromFile(Context ctx, string path)
821
        {
822
823
            using var stream = ctx.FileSystem.OpenForReading(path);

824
825
826
            Debug.Assert(ctx.OpcodeDict != null);
            var parser = new ZapParser(ctx, ctx.OpcodeDict);
            var result = parser.Parse(stream, path);
827

828
829
            if (result.NumberOfSyntaxErrors > 0)
                Errors.ThrowFatal("syntax error");
830

831
            return result.Lines;
832
833
        }

834
        static Symbol EvalExpr(Context ctx, AsmExpr node)
835
        {
Jesse McGrew's avatar
Jesse McGrew committed
836
            switch (node)
837
            {
Jesse McGrew's avatar
Jesse McGrew committed
838
                case NumericLiteral num:
839
                    return new Symbol(num.Value);
Jesse McGrew's avatar
Jesse McGrew committed
840
841

                case SymbolExpr sym:
Jesse McGrew's avatar
Jesse McGrew committed
842
                    if (!ctx.LocalSymbols.TryGetValue(sym.Text, out var result) &&
Jesse McGrew's avatar
Jesse McGrew committed
843
                        !ctx.GlobalSymbols.TryGetValue(sym.Text, out result))
844
                    {
Jesse McGrew's avatar
Jesse McGrew committed
845
846
                        if (ctx.FinalPass)
                        {
847
                            Errors.ThrowFatal(node, "undefined symbol: {0}", sym.Text);
Jesse McGrew's avatar
Jesse McGrew committed
848
849
850
851
852
853
                        }
                        else
                        {
                            result = new Symbol(sym.Text, SymbolType.Unknown, 0);
                            ctx.GlobalSymbols.Add(sym.Text, result);
                        }
854
                    }
Jesse McGrew's avatar
Jesse McGrew committed
855
856
857
858
859
860
861
862

                    return result;

                case AdditionExpr add:
                    var left = EvalExpr(ctx, add.Left);
                    var right = EvalExpr(ctx, add.Right);
                    if (!ctx.FinalPass &&
                        (left.Type == SymbolType.Unknown || right.Type == SymbolType.Unknown))
863
                    {
Jesse McGrew's avatar
Jesse McGrew committed
864
                        return new Symbol(null, SymbolType.Unknown, 0);
865
                    }
866
                    if (Addable(left.Type) && Addable(right.Type))
Jesse McGrew's avatar
Jesse McGrew committed
867
                        return new Symbol(null, SymbolType.Constant, left.Value + right.Value);
868
869
870
                    throw new NotImplementedException($"Unimplemented symbol addition: {left.Type} + {right.Type}");

                    // we can add numbers and non-packed addresses
Jesse McGrew's avatar
Jesse McGrew committed
871
                    static bool Addable(SymbolType type) => type switch
872
                    {
Jesse McGrew's avatar
Jesse McGrew committed
873
874
875
                        SymbolType.Constant or SymbolType.Label or SymbolType.Object => true,
                        _ => false,
                    };
876

Jesse McGrew's avatar
Jesse McGrew committed
877
878
                default:
                    throw new NotImplementedException();
879
880
881
            }
        }

882
        static void EvalOperand(Context ctx, AsmExpr node, out byte type, out ushort value, out Fixup? fixup)
883
        {
884
            EvalOperand(ctx, node, out type, out value, out fixup, false);
885
886
        }

887
        static void EvalOperand(Context ctx, AsmExpr node, out byte type, out ushort value, out Fixup? fixup,
888
889
            bool allowLocalLabel)
        {
890
891
            fixup = null;

892
            bool apos = false;
Jesse McGrew's avatar
Jesse McGrew committed
893
            if (node is QuoteExpr quote)
894
895
            {
                apos = true;
Jesse McGrew's avatar
Jesse McGrew committed
896
                node = quote.Inner;
897
898
            }

899
            if (node is SymbolExpr symExpr)
900
            {
901
                var text = symExpr.Text;
Jesse McGrew's avatar
Jesse McGrew committed
902
                if (ctx.LocalSymbols.TryGetValue(text, out var sym))
903
904
                {
                    if (sym.Type == SymbolType.Label)
905
                    {
906
907
908
909
910
911
912
913
                        if (!allowLocalLabel)
                            Errors.Serious(ctx, node, "local label used as operand");

                        // note: returned value is relative to the current position,
                        // which is probably not where the value will be written.
                        // caller must correct it.
                        type = OPERAND_WORD;
                        value = (ushort)(sym.Value - ctx.Position);
914
915
916
                    }
                    else
                    {
917
                        Debug.Assert(sym.Type == SymbolType.Variable);
918
919
                        type = OPERAND_VAR;
                        value = (byte)sym.Value;
920
                    }
921
                }
922
                else if (ctx.GlobalSymbols.TryGetValue(text, out sym))
923
924
925
926
927
928
929
930
931
932
933
                {
                    if (sym.Type == SymbolType.Variable)
                        type = OPERAND_VAR;
                    else if ((sym.Value & 0xff) == sym.Value)
                        type = OPERAND_BYTE;
                    else
                        type = OPERAND_WORD;
                    value = (ushort)sym.Value;
                }
                else if (ctx.FinalPass && !allowLocalLabel)
                {
934
                    Errors.ThrowFatal(node, "undefined symbol: {0}", text);
935
                    // ReSharper disable once HeuristicUnreachableCode
936
937
938
939
940
941
942
943
944
                    type = 0;
                    value = 0;
                }
                else
                {
                    // not defined yet
                    type = OPERAND_BYTE;
                    value = 0;
                    if (allowLocalLabel)
945
                        ctx.MarkUnknownBranch(text);
946
                    else
947
                        fixup = new Fixup(text);
948
949
950
951
                }
            }
            else
            {
952
953
                var sym = EvalExpr(ctx, node);
                value = (ushort)sym.Value;
954
                type = value < 256 ? OPERAND_BYTE : OPERAND_WORD;
955
956
957
958
959
960
            }

            if (apos)
                type = OPERAND_BYTE;
        }

961
        static bool IsLongConstant(Context ctx, AsmExpr node)
962
        {
Jesse McGrew's avatar
Jesse McGrew committed
963
            switch (node)
964
            {
Jesse McGrew's avatar
Jesse McGrew committed
965
                case NumericLiteral num:
966
                    return ((uint)num.Value & 0xffffff00) != 0;
Jesse McGrew's avatar
Jesse McGrew committed
967
968

                case SymbolExpr symExpr:
Jesse McGrew's avatar
Jesse McGrew committed
969
                    if (ctx.LocalSymbols.TryGetValue(symExpr.Text, out _))
Jesse McGrew's avatar
Jesse McGrew committed
970
971
972
973
                    {
                        // the only legal local symbol operand is a local variable
                        return false;
                    }
Jesse McGrew's avatar
Jesse McGrew committed
974
                    else if (ctx.GlobalSymbols.TryGetValue(symExpr.Text, out var sym))
Jesse McGrew's avatar
Jesse McGrew committed
975
976
977
978
979
980
981
982
983
984
985
986
                    {
                        return sym.Value < 0 || sym.Value > 255;
                    }
                    else
                    {
                        // not defined yet, assume it's a faraway global label
                        // (could also be a local label if we're assembling a JUMP instruction,
                        // but that's assembled as a word anyway)
                        return true;
                    }

                case AdditionExpr add:
987
988
989
990
991
                    if (IsLongConstant(ctx, add.Left) || IsLongConstant(ctx, add.Right))
                        return true;
                    var left = EvalExpr(ctx, add.Left);
                    var right = EvalExpr(ctx, add.Right);
                    return ((uint)(left.Value + right.Value) & 0xffffff00) != 0;
Jesse McGrew's avatar
Jesse McGrew committed
992
993
994

                case StringLiteral _:
                case QuoteExpr _:
995
996
                    return false;

Jesse McGrew's avatar
Jesse McGrew committed
997
998
                default:
                    throw new ArgumentException("Unexpected expr type: " + node.GetType().Name, nameof(node));
999
1000
1001
            }
        }

1002
        static void HandleDirective(Context ctx, AsmLine node, int nodeIndex, bool assembling)
1003
1004
        {
            // local scope is terminated by any directive except .DEBUG_LINE (not counting labels)
1005
            if (!(node is DebugLineDirective))
1006
                ctx.EndReassemblyScope(nodeIndex);
1007

Jesse McGrew's avatar
Jesse McGrew committed
1008
            switch (node)
1009
            {
1010
1011
1012
1013
                case NullDirective _:
                    // nada
                    break;

Jesse McGrew's avatar
Jesse McGrew committed
1014
1015
1016
1017
1018
                case NewDirective _:
                case TimeDirective _:
                case SoundDirective _:
                    // these are explicitly handled by PassOne or PassTwo
                    break;
1019

Jesse McGrew's avatar
Jesse McGrew committed
1020
1021
1022
                case LangDirective langNode:
                    ctx.SetLanguage(EvalExpr(ctx, langNode.LanguageId).Value, EvalExpr(ctx, langNode.EscapeChar).Value);
                    break;
1023

Jesse McGrew's avatar
Jesse McGrew committed
1024
1025
                case ChrsetDirective chrsetNode:
                    if (ctx.ZVersion >= 5)
1026
                    {
Jesse McGrew's avatar
Jesse McGrew committed
1027
                        int charsetNum = EvalExpr(ctx, chrsetNode.CharsetNum).Value;
1028

Jesse McGrew's avatar
Jesse McGrew committed
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
                        switch (charsetNum)
                        {
                            case 0:
                            case 1:
                            case 2:
                                ctx.StringEncoder.SetCharset(
                                    charsetNum,
                                    chrsetNode.Characters.Select(e => (byte)EvalExpr(ctx, e).Value));
                                break;

                            default:
                                Errors.ThrowFatal("no such character set");
                                break;
                        }
1043
                    }
1044
                    else
Jesse McGrew's avatar
Jesse McGrew committed
1045
1046
1047
1048
1049
1050
                    {
                        Errors.ThrowFatal(".CHRSET is only supported in Z-machine versions 5-8");
                    }
                    break;

                case FunctDirective functNode:
1051
                    BeginFunction(ctx, functNode, nodeIndex);
Jesse McGrew's avatar
Jesse McGrew committed
1052
1053
                    break;

Jesse McGrew's avatar
Jesse McGrew committed
1054
1055
1056
1057
1058
1059
1060
1061
1062
                case AlignDirective alignNode:
                    var divisor = EvalExpr(ctx, alignNode.Divisor);
                    if (divisor.Type != SymbolType.Constant)
                    {
                        Errors.ThrowSerious(alignNode.Divisor, "non-constant argument to .ALIGN");
                    }
                    AlignUnpacked(ctx, divisor.Value);
                    break;

Jesse McGrew's avatar
Jesse McGrew committed
1063
1064
                case TableDirective tableNode:
                    if (ctx.TableStart != null)
Jesse McGrew's avatar
Jesse McGrew committed
1065
                    {
1066
                        Errors.Warn(ctx, node, "starting new table before ending old table");
Jesse McGrew's avatar
Jesse McGrew committed
1067
                    }
Jesse McGrew's avatar
Jesse McGrew committed
1068
1069
1070
1071
1072
1073
                    ctx.TableStart = ctx.Position;
                    ctx.TableSize = null;
                    if (tableNode.Size != null)
                    {
                        var sym = EvalExpr(ctx, tableNode.Size);
                        if (sym.Type != SymbolType.Constant)
1074
                            Errors.Warn(ctx, node, "ignoring non-constant table size specifier");
Jesse McGrew's avatar
Jesse McGrew committed
1075
1076
1077
1078
1079
1080
1081
                        else
                            ctx.TableSize = sym.Value;
                    }
                    break;

                case EndtDirective _:
                    if (ctx.TableStart == null)
1082
1083
1084
1085
                    {
                        Errors.Warn(ctx, node, "ignoring .ENDT outside of a table definition");
                    }
                    else if (ctx.TableSize != null)
Jesse McGrew's avatar
Jesse McGrew committed
1086
1087
                    {
                        if (ctx.Position - ctx.TableStart.Value != ctx.TableSize.Value)
1088
                            Errors.Warn(ctx, node, "incorrect table size: expected {0}, actual {1}",
Jesse McGrew's avatar
Jesse McGrew committed
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
                                ctx.TableSize.Value,
                                ctx.Position - ctx.TableStart.Value);
                    }
                    ctx.TableStart = null;
                    ctx.TableSize = null;
                    break;

                case VocbegDirective vocbegNode:
                    if (ctx.InVocab)
                    {
1099
                        Errors.Warn(ctx, node, "ignoring .VOCBEG inside another vocabulary block");
Jesse McGrew's avatar
Jesse McGrew committed
1100
                    }
1101
1102
                    else
                    {
Jesse McGrew's avatar
Jesse McGrew committed
1103
1104
1105
                        var sym1 = EvalExpr(ctx, vocbegNode.RecordSize);
                        var sym2 = EvalExpr(ctx, vocbegNode.KeySize);
                        if (sym1.Type != SymbolType.Constant || sym2.Type != SymbolType.Constant)
1106
                            Errors.Warn(ctx, node, "ignoring .VOCBEG with non-constant size specifiers");
Jesse McGrew's avatar
Jesse McGrew committed
1107
1108
                        else
                            ctx.EnterVocab(sym1.Value, sym2.Value);
1109
                    }
Jesse McGrew's avatar
Jesse McGrew committed
1110
1111
1112
1113
                    break;

                case VocendDirective _:
                    if (!ctx.InVocab)
1114
                        Errors.Warn(ctx, node, "ignoring .VOCEND outside of a vocabulary block");
Jesse McGrew's avatar
Jesse McGrew committed
1115
1116
1117
1118
1119
1120
                    else
                        ctx.LeaveVocab(node);
                    break;

                case DataDirective dataNode:
                    var elements = dataNode.Elements;
1121
                    foreach (var expr in elements)
1122
                    {
1123
                        var sym = EvalExpr(ctx, expr);
Jesse McGrew's avatar
Jesse McGrew committed
1124
                        if (sym.Type == SymbolType.Unknown && ctx.FinalPass)
1125
                        {
1126
                            Errors.ThrowFatal(expr, "unrecognized symbol: " + ((SymbolExpr)expr).Text);
1127
                        }
Jesse McGrew's avatar
Jesse McGrew committed
1128
1129
1130
1131
                        if (node is ByteDirective)
                        {
                            if (sym.Type == SymbolType.Label && ctx.InVocab)
                            {
1132
                                Errors.ThrowFatal(expr, "global label refs inside vocab section must be assembled as words");
Jesse McGrew's avatar
Jesse McGrew committed
1133
                            }
1134

Jesse McGrew's avatar
Jesse McGrew committed
1135
1136
1137
1138
1139
1140
1141
1142
                            ctx.WriteByte((byte)sym.Value);
                        }
                        else
                        {
                            if (sym.Type == SymbolType.Label && ctx.InVocab)
                            {
                                // global labels inside the vocab table need to be fixed up at .VOCEND,
                                // since they may refer to other vocab words
1143
                                Debug.Assert(sym.Name != null);
1144
                                var fixup = new Fixup(sym.Name) { Location = ctx.Position };
Jesse McGrew's avatar
Jesse McGrew committed
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
                                ctx.Fixups.Add(fixup);
                            }

                            ctx.WriteWord((ushort)sym.Value);
                        }
                    }
                    break;

                case FstrDirective fstrNode:
                    AddAbbreviation(ctx, fstrNode);
                    break;

                case GstrDirective fstrNode:
                    PackString(ctx, fstrNode);
                    break;

                case StrDirective strNode:
                    ctx.WriteZString(strNode.Text, false);
                    break;

                case StrlDirective strlNode:
                    ctx.WriteZString(strlNode.Text, true);
                    break;

                case LenDirective lenNode:
                    ctx.WriteZStringLength(lenNode.Text);
                    break;

                case ZwordDirective zwordNode:
                    ctx.WriteZWord(zwordNode.Text);
                    break;

                case EqualsDirective equalsNode:
                    var rvalue = EvalExpr(ctx, equalsNode.Right);
                    if (rvalue.Type == SymbolType.Unknown && ctx.FinalPass)
                    {
                        Errors.ThrowFatal(equalsNode.Right, "unrecognized symbol");
1182
1183
1184
                    }
                    else
                    {
Jesse McGrew's avatar
Jesse McGrew committed
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
                        ctx.GlobalSymbols[equalsNode.Left] = rvalue;
                    }
                    break;

                case GvarDirective gvarNode:
                    ctx.AddGlobalVar(gvarNode.Name);
                    if (gvarNode.InitialValue != null)
                    {
                        var sym = EvalExpr(ctx, gvarNode.InitialValue);
                        if (sym.Type == SymbolType.Unknown && ctx.FinalPass)
1195
                        {
Jesse McGrew's avatar
Jesse McGrew committed
1196
                            Errors.ThrowFatal(gvarNode.InitialValue, "unrecognized symbol");
1197
                        }
1198
1199
                        ctx.WriteWord((ushort)sym.Value);
                    }
Jesse McGrew's avatar
Jesse McGrew committed
1200
                    else
1201
                    {
Jesse McGrew's avatar
Jesse McGrew committed
1202
                        ctx.WriteWord(0);
1203
                    }
Jesse McGrew's avatar
Jesse McGrew committed
1204
                    break;
1205

Jesse McGrew's avatar
Jesse McGrew committed
1206
1207
                case ObjectDirective od:
                    ctx.AddObject(od.Name);
1208
                    const string ObjectVersionError = "wrong .OBJECT syntax for this version";
Jesse McGrew's avatar
Jesse McGrew committed
1209
1210
1211
1212
1213
                    if (ctx<