<* PRAGMA LL *> MODULEEditor EXPORTSEditor ,JunoHandleLexErr ,EditorUI ; IMPORT JunoError, View, Drawing, ToolBox, EditorXtra, JunoConfig; IMPORT JunoParse, JunoLex, JunoAST, JunoUnparse, JunoToken; IMPORT JunoScope, JunoCompile; FROM JunoCompileErr IMPORT Error, Raise; IMPORT JunoRT; IMPORT TextPort; IMPORT VBT, Rect, Axis, TextVBT, HVSplit, Split; IMPORT Rd, Wr, Formatter, TextRd, TextWr, Text, Atom, Lex, Fmt, FloatMode; IMPORT AtomAtomTbl, AtomRefTbl; FROM Thread IMPORT Alerted; <* FATAL Rd.Failure, Wr.Failure, Alerted *> CONST MinUnparseWidth = 15; CmdPrefix = "Cmd"; REVEAL T = Public BRANDED "Editor.T" OBJECT trees, lastTree: Forest := NIL; currentTree: Forest; treesValid, textPretty := FALSE; width: INTEGER := -1; maxCurrCmd: INTEGER := -1; toolTypes: AtomAtomTbl.T; setMenus: AtomRefTbl.T; OVERRIDES init := Init; reshape := Reshape; shape := Shape; modified := Modified; txtModified := NoOp; getToolType := GetToolType; getMenu := GetMenu; END; (* An "Editor.T" is an editor for a Juno module. If "t: T", then "t.trees" is the list of parse trees for the top-level blocks of the module. "t.trees" holds the truth iff "t.treesValid". The value of "currentTree" points at the tree containing the top line of the textport; it is set by Parse and maintained by Unparse, and is valid only if "treesValid" is true. It is used to prevent the textport from scrolling undesirably when the user reshapes the editor or remakes it by clicking "Run". The boolean "t.textPretty" is TRUE iff the editor contains the result of unparsing "t.trees"; note that "t.textPretty => t.treesValid". Hence there are 3 combinations for the two booleans: | Valid Pretty Meaning | F F the editor contains the truth, and is not pretty-printed | T F "t.trees" is the result of successfully parsing the source | T T the editor contains the unparsed version of "t.trees" If "t.textPretty", then "t.width" is the width at which the trees were unparsed. If the trees were unparsed into an empty window, then "t.textPretty" is TRUE, and "t.width = -1". The editor also implements an abstract "current command stack". The procedures declared in the editor with names of the form "CmdPrefix & Fmt.Int(X)", where "CmdPrefix" is a global constant and "X" is a non-negative integer, are on the stack. The value "X" is called the index of the current command. "t.maxCurrCmd" is the value of the maximum current command index; this is the current command on the top of the stack. The stack is empty iff "t.maxCurrCmd = -1". *) Forest = ForestPublic BRANDED "Editor.Forest" OBJECT start, end: CARDINAL; END; (* For each tree "t" in the list of trees, "t.start" and "t.end" are the indices in the module editor of the first and last character of the unparsed version of "t". This interval includes the whitespace charcters following "t". *) PROCEDURENoOp (<*UNUSED*> tp: T) = BEGIN END NoOp; PROCEDUREInit (tp: T; src: TEXT; readOnly := FALSE): T = BEGIN EVAL TextPort.T.init(tp, font := JunoConfig.codeFont, wrap := FALSE, readOnly := readOnly); TextPort.SetModified(tp, TRUE); TextPort.SetText(tp, src); TextPort.SetModified(tp, FALSE); tp.toolTypes := NEW(AtomAtomTbl.Default).init(); tp.setMenus := NEW(AtomRefTbl.Default).init(); RETURN tp END Init; PROCEDUREScrollToCurrentTree (tp: T) = VAR pos: INTEGER; BEGIN IF tp.currentTree = NIL THEN pos := 0 ELSE pos := tp.currentTree.start END; EditorXtra.IndexToTop(tp, pos) END ScrollToCurrentTree; PROCEDURESetCurrentTree (tp: T) = VAR cpos := EditorXtra.TopLineIndex(tp); f := tp.trees; BEGIN WHILE f # NIL AND f.end <= cpos DO f := f.next END; tp.currentTree := f END SetCurrentTree; PROCEDUREReshape (tp: T; READONLY cd: VBT.ReshapeRec) = <* LL.sup = VBT.mu.tp *> BEGIN IF Rect.IsEmpty(cd.new) THEN tp.width := -1 ELSE VAR width := Width(tp); BEGIN IF tp.treesValid AND (NOT tp.textPretty OR width # tp.width) THEN SetCurrentTree(tp); Unparse2(tp, width); ScrollToCurrentTree(tp) END END END; TextPort.T.reshape(tp, cd) END Reshape; PROCEDUREShape (tp: T; ax: Axis.T; n: CARDINAL): VBT.SizeRange = VAR res := TextPort.T.shape(tp, ax, n); BEGIN res.lo := 0; res.hi := VBT.DefaultShape.hi; RETURN res END Shape; PROCEDUREModified (tp: T) = <* LL.sup < VBT.mu *> BEGIN TextPort.T.modified(tp); tp.treesValid := FALSE; tp.textPretty := FALSE; tp.txtModified() END Modified; PROCEDUREGetToolType (ed: T; nm: Atom.T; VAR (*OUT*) type: Atom.T): BOOLEAN = BEGIN RETURN ed.toolTypes.get(nm, type) END GetToolType; PROCEDUREGetMenu (ed: T; nm: Atom.T): VBT.T = VAR menuRef: REFANY; BEGIN IF ed.setMenus.get(nm, menuRef) THEN RETURN menuRef ELSE RETURN TextVBT.New("No parameters have been defined") END END GetMenu; PROCEDURETrees (tp: T): Forest = BEGIN RETURN tp.trees END Trees; PROCEDUREValid (tp: T): BOOLEAN = BEGIN RETURN tp.treesValid END Valid; PROCEDUREHandleLexErr ( err: JunoLex.ErrorRec; rd: Rd.T; wr: Wr.T; VAR (*OUT*) start, finish: INTEGER) = BEGIN Wr.PutText(wr, "\n"); CASE err.kind OF <* NOWARN *> | JunoLex.ErrorKind.UnclosedComment, JunoLex.ErrorKind.UnclosedText => start := Wr.Index(wr); Wr.PutText(wr, err.initialChars); Wr.PutText(wr, Rd.GetText(rd, LAST(CARDINAL))); finish := Wr.Index(wr) | JunoLex.ErrorKind.BadInitialChar, JunoLex.ErrorKind.BadEscapeChar, JunoLex.ErrorKind.BadReal => Wr.PutText(wr, err.initialChars); start := Wr.Index(wr); finish := start + 1; Wr.PutText(wr, Rd.GetText(rd, LAST(CARDINAL))); IF start = Wr.Index(wr) THEN Wr.PutChar(wr, ' ') END END END HandleLexErr; PROCEDURECurrCmdIndex (ast: JunoAST.T): INTEGER =
Ifastis a procedure declaration for a procedure whose name has the value of the global constantCmdPrefixas a prefix, then return the value of the suffix; otherwise, return -1.
  BEGIN
    TYPECASE ast OF
      JunoAST.ProcDecl (pd) =>
        VAR
          procName := Atom.ToText(pd.header.name);
          prefixLen := Text.Length(CmdPrefix);
          res: INTEGER;
        BEGIN
          IF Text.Equal(CmdPrefix, Text.Sub(procName, 0, prefixLen)) THEN
            TRY res := Lex.Int(TextRd.New(Text.Sub(procName, prefixLen)))
            EXCEPT Lex.Error, FloatMode.Trap, Rd.Failure => res := -1
            END;
            RETURN res
          END
        END
    ELSE (* SKIP *)
    END;
    RETURN -1
  END CurrCmdIndex;
PROCEDURE Parse (tp: T; time: VBT.TimeStamp): BOOLEAN =
  BEGIN
    IF tp.treesValid THEN RETURN TRUE END;
    RETURN Parse2(tp, time)
  END Parse;
PROCEDURE Parse2 (tp: T; time: VBT.TimeStamp): BOOLEAN =
  VAR
    errmsg: TEXT;
    rd := TextRd.New(TextPort.GetText(tp));
    wr: TextWr.T;
    w := Width(tp);
    start, finish := -1;
    cpos := EditorXtra.TopLineIndex(tp);
    ip: JunoParse.IterativeParse;
  <*FATAL Rd.Failure, Wr.Failure *>
  BEGIN
    TRY ip := JunoParse.StartIterativeParse(rd) EXCEPT
      JunoLex.Error (err) =>
      	wr := TextWr.New();
      	errmsg := JunoLex.ErrorText(err.kind);
      	HandleLexErr(err, rd, wr, start, finish);
      	ip := NIL
    END;
    tp.trees := NIL;
    tp.lastTree := NIL;
    tp.currentTree := NIL;
    tp.maxCurrCmd := -1;
    IF ip # NIL THEN
  	LOOP
  	  VAR ast: JunoAST.Block; tokens: CARDINAL; BEGIN
  	    TRY
  	      JunoParse.Block(ip, ast, tokens)
  	    EXCEPT
  	      JunoLex.Error (err) =>
              	wr := TextWr.New();
              	UnparseTrees(tp.trees, wr, w);
  		IF ast # NIL THEN
  		  JunoUnparse.Block(wr, ast, tokens,
                    indent := 0, width := w, prec := JunoConfig.realPrec)
  		END;
              	errmsg := JunoLex.ErrorText(err.kind);
  		  HandleLexErr(err, rd, wr, start, finish);
              	EXIT
  	    | JunoParse.Error(err) =>
                wr := TextWr.New();
                UnparseTrees(tp.trees, wr, w);
  		IF ast # NIL THEN
  		  JunoUnparse.Block(wr, ast, tokens,
                    indent := 0, width := w, prec := JunoConfig.realPrec)
  		END;
  		Wr.PutChar(wr, '\n');
  		errmsg := "Parse error";
  		IF err.expected # JunoToken.Kind.Unknown THEN
  		  errmsg := errmsg & " (expected "
  		    & JunoToken.KindName[err.expected] & ")"
  		END;
  		start := Wr.Index(wr);
  		Wr.PutText(wr, JunoToken.ToText(err.found));
  		finish := Wr.Index(wr);
  		Wr.PutChar(wr, ' ');
  		Wr.PutText(wr, err.additional);
  		Wr.PutText(wr, Rd.GetText(rd, LAST(CARDINAL)));
  		EXIT
  	    END;
  	    IF ast = NIL THEN EXIT END;
            (* Next, append "ast" to the list ending at "tp.lastTree".  Also,
               if it is the first parsed item that ends past the top line of
               the textport, record it in "currentTree". *)
            VAR f := NEW(Forest, tree := ast, next := NIL); BEGIN
              AppendTree(tp, f);
  	      IF tp.currentTree = NIL AND JunoParse.GetIndex(ip) > cpos THEN
  	        tp.currentTree := f
  	      END
  	    END;
            tp.maxCurrCmd := MAX(tp.maxCurrCmd, CurrCmdIndex(ast))
  	  END
  	END;                      (* loop *)
  	JunoParse.FinishIterativeParse(ip)
    END;                        (* if *)
    IF start # finish THEN
      <* ASSERT start # -1 AND finish # -1 *>
      TextPort.SetModified(tp, TRUE);
      TextPort.SetText(tp, TextWr.ToText(wr));
      TextPort.SetModified(tp, FALSE);
      Wr.Close(wr);
      JunoError.P(tp, errmsg, start, finish, time);
      RETURN FALSE
    ELSE
      TextPort.SetModified(tp, FALSE);
      TextPort.Normalize(tp, cpos);
      tp.treesValid := TRUE;
      RETURN TRUE
    END
  END Parse2;
PROCEDURE Unparse (tp: T; errast: JunoAST.T := NIL;
  msg: TEXT := NIL; time: VBT.TimeStamp := 0) =
  <* LL.sup < tp *>
  BEGIN
    <* ASSERT tp.treesValid *>
    <* ASSERT (errast # NIL) = (msg # NIL) *>
    IF Rect.IsEmpty(VBT.Domain(tp)) THEN
      tp.width := -1
    ELSE
      VAR width := Width(tp); BEGIN
        IF NOT tp.textPretty OR width # tp.width THEN
          Unparse2(tp, width, errast, msg, time)
        END
      END;
      IF errast = NIL THEN ScrollToCurrentTree(tp) END
    END
  END Unparse;
PROCEDURE Unparse2 (tp: T; width: CARDINAL;
  errast: JunoAST.T := NIL; msg: TEXT := NIL; time: VBT.TimeStamp := 0) =
  <* FATAL Wr.Failure *>
  VAR wr := TextWr.New(); BEGIN
    UnparseTrees(tp.trees, wr, width, errast := errast);
    VAR txt := TextWr.ToText(wr); start, finish: INTEGER; BEGIN
      IF errast # NIL THEN
  	start := Text.FindChar(txt, '\001');
        finish := Text.FindChar(txt, '\002');
        <* ASSERT start # -1 AND finish # -1 *>
  	txt := Text.Sub(txt, 0, start)
  	  & Text.Sub(txt, start + 1, finish - start - 1)
  	  & Text.Sub(txt, finish + 1)
      END;
      TextPort.SetModified(tp, TRUE);
      TextPort.SetText(tp, txt);
      TextPort.SetModified(tp, FALSE);
      IF errast # NIL THEN
        VAR t := tp.trees; BEGIN
          (* update the "start" and "end" values for blocks appearing after
             the erroneous tree "errast" *)
          WHILE t # NIL DO
            IF t.start > finish THEN
              DEC(t.start, 2); DEC(t.end, 2)
            ELSIF t.end > finish THEN
              DEC(t.end, 2)
            END;
            t := t.next
          END
        END;
        IF time # 0 THEN
          JunoError.P(tp, msg, start, finish - 1, time)
        END
      END;
      Wr.Close(wr)
    END;
    tp.textPretty := TRUE;
    tp.width := width
  END Unparse2;
PROCEDURE UnparseTrees (f: Forest; wr: Wr.T;
  width: CARDINAL; errast: JunoAST.T  := NIL) =
Unparse the list of treesftowrat the widthwidth. Iferrast # NIL, then bracket the unparsing oferrastby the characters '\001' and '\002' when unparsing. This procedure also sets thestartandendfields of each tree inf.
  VAR fmt := Formatter.New(wr, width); BEGIN
    WHILE f # NIL DO
      f.start := Wr.Index(wr);
      JunoUnparse.ToFmt(fmt, f.tree, indent := 0,
        prec := JunoConfig.realPrec, errast := errast);
      Formatter.NewLine(fmt, freshLine := FALSE);
      (* Print a second newline so long as it would not separate two
         consecutive UI declarations. *)
      IF NOT (ISTYPE(f.tree, JunoAST.UIDecl) AND f.next # NIL AND
              ISTYPE(f.next.tree, JunoAST.UIDecl)) THEN
        Formatter.NewLine(fmt, freshLine := FALSE)
      END;
      Formatter.Flush(fmt);
      f.end := Wr.Index(wr);
      f := f.next
    END;
    Formatter.Close(fmt)
  END UnparseTrees;
PROCEDURE AppendTree (tp: T; t: Forest) =
Appendttotp's list of trees.
  BEGIN
    IF tp.trees = NIL
      THEN tp.trees := t
      ELSE tp.lastTree.next := t
    END;
    tp.lastTree := t
  END AppendTree;
PROCEDURE AddTree (ed: T; ast: JunoAST.T) =
  VAR t := NEW(Forest, tree := ast); BEGIN
    AppendTree(ed, t);
    ed.maxCurrCmd := MAX(ed.maxCurrCmd, CurrCmdIndex(ast));
    (* unparse the new tree to the end of the editor *)
    VAR wr := TextWr.New(); BEGIN
      t.start := TextPort.Length(ed);
      JunoUnparse.P(wr, ast, 0, Width(ed),
        prec := JunoConfig.realPrec, errast := NIL);
      Wr.PutText(wr, "\n\n");
      VAR wasMod := TextPort.IsModified(ed); BEGIN
      	TextPort.SetModified(ed, TRUE);
      	TextPort.PutText(ed, TextWr.ToText(wr));
      	t.end := TextPort.Length(ed);
      	TextPort.Normalize(ed, t.start);
      	TextPort.SetModified(ed, wasMod)
      END;
      Wr.Close(wr)
    END;
    (* update the editor's tables for a UIDecl *)
    TYPECASE ast OF JunoAST.UIDecl (ui) =>
      <* FATAL Error *> BEGIN
      	IF ed.toolTypes.put(FirstName(ui, 1).id1, ui.name) THEN
      	  <* ASSERT FALSE *>
        END
      END
    ELSE (* SKIP *)
    END
  END AddTree;
PROCEDURE NextCmdNum (ed: T): CARDINAL =
  BEGIN RETURN ed.maxCurrCmd + 1 END NextCmdNum;
PROCEDURE NextCmdName (ed: T): Atom.T =
  BEGIN
    RETURN Atom.FromText(CmdPrefix & Fmt.Int(NextCmdNum(ed)))
  END NextCmdName;
PROCEDURE PopCurrCmd (ed: T; VAR (*OUT*) nm: JunoAST.Id): JunoAST.Cmd =
  <* LL.sup <= VBT.mu *>
  BEGIN
    IF NOT ed.treesValid OR ed.maxCurrCmd < 0 THEN RETURN NIL END;
    Unparse(ed);
    VAR t: Forest := NIL; BEGIN
      VAR curr := ed.trees; prev: Forest := NIL; max := -1; BEGIN
      	(* Set "t" to the tree to delete, set "max" to the new current
           command maximum, and set "prev" to the tree before "t" (or
           "NIL" if "t" is the first tree in the list). *)
        WHILE curr # NIL DO
          VAR ix := CurrCmdIndex(curr.tree); BEGIN
            IF ix = ed.maxCurrCmd
              THEN t := curr
              ELSE max := MAX(max, ix)
            END
          END;
          IF t = NIL THEN prev := curr END;
          curr := curr.next
      	END;
        ed.maxCurrCmd := max;
      	(* remove "t" from "trees[ed]" *)
        IF ed.lastTree = t THEN
          ed.lastTree := prev
        END;
      	IF prev = NIL
      	  THEN ed.trees := t.next
      	  ELSE prev.next := t.next
      	END;
      END;
      (* delete the text for "t" from "src[ed]" *)
      VAR wasMod := TextPort.IsModified(ed); BEGIN
        TextPort.SetModified(ed, TRUE);
        TextPort.Replace(ed, t.start, t.end, "");
        TextPort.SetModified(ed, wasMod)
      END;
      (* return the procedure body *)
      VAR decl := NARROW(t.tree, JunoAST.ProcDecl); body := decl.body; BEGIN
        nm := decl.header.name;
        TYPECASE body OF JunoAST.If (if) =>
          IF ISTYPE(if.body, JunoAST.Proj)
            THEN RETURN if.body
            ELSE RETURN body
          END
        ELSE RETURN body
        END
      END
    END
  END PopCurrCmd;
PROCEDURE Width (ed: TextPort.T): CARDINAL =
  VAR res := VBT.TextWidth(ed, "m", ed.getFont()); BEGIN
    IF res # 0 THEN
      res := Rect.HorSize(VBT.Domain(ed)) DIV res - 2;
    END;
    RETURN MAX(MinUnparseWidth, res)
  END Width;
PROCEDURE ModuleName (ed: T): Atom.T =
  VAR first := ed.trees; BEGIN
    WHILE first # NIL DO
      TYPECASE first.tree OF
        JunoAST.Module (m) => RETURN m.name
      | JunoAST.Comment => first := first.next
      ELSE EXIT
      END
    END;
    RETURN NIL
  END ModuleName;
VAR (* CONST *)
  global_mod := Atom.FromText("_GLOBAL_MOD");
  global_cmd := Atom.FromText("_GLOBAL_CMD");
  global_slot := JunoRT.GetCodeIndex(JunoRT.ProcAttr{
    global_mod, global_cmd, JunoRT.Sig{0,0,0}});
PROCEDURE ProcessExecRes (READONLY res: JunoRT.ExecRes; error_ast: JunoAST.T)
    RAISES {Error} =
RaisesErrorifres.trapCode # JunoRT.TrapCode.NormalHalt, with a message constructed fromres.errorCode, and with error ASTerror_ast.
  BEGIN
    IF res.trapCode # JunoRT.TrapCode.NormalHalt THEN
      Raise(JunoRT.TrapMessage(res), error_ast)
    END
  END ProcessExecRes;
PROCEDURE Pass0 (
    VAR forest: Forest;
    scp: JunoScope.T;
    uniqueModName: BOOLEAN;
    VAR (*OUT*) mod: JunoAST.Id)
  : JunoScope.T RAISES {Error} =
Process theMODULEandIMPORTdeclarations inforest, setmodto the name of the initial module declaration (or NIL if there is none), and setforestto point to the first declaration after the longest prefix of the form<comment>* <module> (<comment> | <import>)*.Returns a restricted version of
scpas determined by any IMPORT statements, orscpitself if there were no IMPORT statements. This implementation assumes that all bundled modules are defined inscp, and that identifiers forBuiltIn.junoare defined in proper ancestor scopes ofscp.If
uniqueModName = TRUE, then any specified module name must not appear inscp; if it does,Erroris raised. Similarly, any modules specified in anIMPORTstatement must appear inscp; if they do not,Erroris raised.
  VAR res: JunoScope.T := NIL; BEGIN
    mod := NIL;
    WHILE forest # NIL DO
      TYPECASE forest.tree OF
      | JunoAST.Comment => (* SKIP *)
      | JunoAST.Module (md) =>
          IF mod # NIL THEN EXIT END;
          IF uniqueModName AND JunoScope.Lookup(scp, md.name) # NIL THEN
            Raise("A \"" & Atom.ToText(md.name) &
              "\" module is already defined", md)
          END;
          mod := md.name
      ELSE EXIT
      END;
      forest := forest.next
    END;
    WHILE forest # NIL DO
      TYPECASE forest.tree OF
        JunoAST.Comment => (* SKIP *)
      | JunoAST.Import (imp) =>
          (* form new scope if necessary *)
          IF res = NIL THEN res := JunoScope.New(JunoScope.Parent(scp)) END;
          (* copy imported modules bound in "scp" to "res" *)
          VAR curr := imp.idList.head; ent: JunoScope.Entity; BEGIN
            WHILE curr # NIL DO
              ent := JunoScope.Lookup(scp, curr.id, localOnly := TRUE);
              IF ent = NIL THEN
            	Raise("\""& Atom.ToText(curr.id) &"\" is not a bundled module",
            	  imp.idList)
              END;
              TRY JunoScope.Bind(res, curr.id, ent) EXCEPT
                JunoScope.NameClash =>
                  Raise("\""& Atom.ToText(curr.id) &"\" repeated in IMPORTs",
            	    imp.idList)
              END;
              curr := curr.next
            END
          END
      ELSE EXIT
      END;
      forest := forest.next
    END;
    IF res = NIL
      THEN RETURN scp
      ELSE RETURN res
    END
  END Pass0;
PROCEDURE Pass1 (
    forest: Forest;
    public, scp: JunoScope.T;
    mod: JunoAST.Id)
    RAISES {Error} =
Pass1 processes the top-level declarations inforestfor the module namedmod.Forestis assumed to have been produced by Pass0, so it does not contain the MODULE and IMPORT declarations at the start of the module. Pass1 treats each type of top-level declaration as follows:(* Comment
      Skip.
|    MODULE, IMPORT     Raise Error.
|    CONST, VAR, PROC   Only install entries in "scp" (and "public").
|    PRED, FUNC         Install entries in "scp" (and "public") and compile
|                         bodies in order of occurrence.
|    UI                 Skip.
|
   Entries are only installed in the "public" scope if the declaration is not
   PRIVATE. *)
  <* FATAL JunoScope.NameClash *>
  BEGIN
    WHILE forest # NIL DO
      TYPECASE forest.tree OF <*NOWARN*>
      | JunoAST.Module (md) =>
          IF mod = NIL
            THEN Raise("MODULE header not at start of file", md)
            ELSE Raise("Only one MODULE header is allowed", md)
          END
      | JunoAST.Import (import) =>
          Raise("IMPORT may only be preceded by MODULE header", import)
      | JunoAST.Comment => (* SKIP *)
      | JunoAST.ConstDecl (cd) =>
          VAR curr := cd.head; BEGIN
            WHILE curr # NIL DO
              IF JunoScope.Lookup(scp, curr.name) # NIL THEN
                Raise("\""&Atom.ToText(curr.name)&"\" is already declared", cd)
              END;
              VAR
                c := NEW(JunoScope.Const, init := curr.value,
                  index := JunoRT.GetVarIndex(mod, curr.name));
              BEGIN
                JunoScope.Bind(scp, curr.name, c);
                IF NOT cd.private THEN
                  JunoScope.Bind(public, curr.name, c)
                END
              END;
              curr := curr.next
            END
          END
      | JunoAST.VarDecl (vd) =>
          VAR curr := vd.head; BEGIN
            WHILE curr # NIL DO
              IF JunoScope.Lookup(scp, curr.name) # NIL THEN
                Raise("\""&Atom.ToText(curr.name)&"\" is already declared", vd)
              END;
              VAR
                v := NEW(JunoScope.Var, init := curr.value,
                  index := JunoRT.GetVarIndex(mod, curr.name));
              BEGIN
                JunoScope.Bind(scp, curr.name, v);
                IF NOT vd.private THEN
                  JunoScope.Bind(public, curr.name, v)
                END
              END;
              curr := curr.next
            END
          END
      | JunoAST.ProcDecl (proc) =>
          WITH pnm = proc.header.name DO
            IF JunoScope.Lookup(scp, pnm) # NIL THEN
              Raise("\"" & Atom.ToText(pnm) & "\" is already declared",
                proc.header)
            END;
            VAR p := JunoScope.NewProc(proc, mod); BEGIN
              JunoScope.Bind(scp, pnm, p);
              IF NOT proc.private THEN JunoScope.Bind(public, pnm, p) END
            END
          END
      | JunoAST.PredDecl (pred) =>
          WITH pnm = pred.header.name DO
            IF JunoScope.Lookup(scp, pnm) # NIL THEN
              Raise("\"" & Atom.ToText(pnm) & "\" is already declared",
                pred.header)
            END;
            VAR p := JunoScope.NewPred(pred, mod); BEGIN
              JunoCompile.PredDecl(pnm, p, scp);
              JunoScope.Bind(scp, pnm, p);
              IF NOT pred.private THEN JunoScope.Bind(public, pnm, p) END
            END
          END
      | JunoAST.FuncDecl (func) =>
          WITH fnm = func.header.name DO
            IF JunoScope.Lookup(scp, fnm) # NIL THEN
              Raise("\"" & Atom.ToText(fnm) & "\" is already declared",
                func.header)
            END;
            VAR f := JunoScope.NewFunc(func, mod); BEGIN
              JunoCompile.FuncDecl(fnm, f, scp);
              JunoScope.Bind(scp, fnm, f);
              IF NOT func.private THEN JunoScope.Bind(public, fnm, f) END
            END
          END
      | JunoAST.UIDecl => (* SKIP *)
      END;
      forest := forest.next
    END
  END Pass1;
PROCEDURE Pass2 (forest: Forest; scp: JunoScope.T) RAISES {Error} =
Compile procedure bodies.
  BEGIN
    WHILE forest # NIL DO
      TYPECASE forest.tree OF
      | JunoAST.ProcDecl(proc) =>
          VAR p: JunoScope.Proc :=
            JunoScope.Lookup(scp, proc.header.name, localOnly := TRUE);
          BEGIN
            EVAL JunoCompile.ProcDecl(proc.header.name, p, scp)
          END
      ELSE (* SKIP *)
      END;
      forest := forest.next
    END
  END Pass2;
PROCEDURE Pass3 (forest: Forest; scp: JunoScope.T) RAISES {Error} =
Compile and run constant and global variable initializers.
  BEGIN
    WHILE forest # NIL DO
      TYPECASE forest.tree OF
      | JunoAST.ConstDecl (cd) =>
          VAR curr := cd.head; BEGIN
            WHILE curr # NIL DO
              VAR
                c: JunoScope.Const := JunoScope.Lookup(
                  scp, curr.name, localOnly := TRUE);
                res_slot: CARDINAL;
              BEGIN
                JunoRT.code_tbl[global_slot] := JunoCompile.Expr(
                  c.init, scp, curr.name, (*OUT*) res_slot, pure := FALSE);
                ProcessExecRes(JunoRT.ExecFromSlot(global_slot), cd);
                JunoRT.value_tbl[c.index] := JunoRT.value_tbl[res_slot]
              END;
              curr := curr.next
            END
          END
      | JunoAST.VarDecl (vd) =>
          VAR curr := vd.head; BEGIN
            WHILE curr # NIL DO
              VAR
                v: JunoScope.Var := JunoScope.Lookup(
                  scp, curr.name, localOnly := TRUE);
                res_slot: CARDINAL;
                init: JunoAST.Expr := v.init;
              BEGIN
                IF init = JunoAST.NilExpr THEN init := JunoAST.NilVal END;
                JunoRT.code_tbl[global_slot] := JunoCompile.Expr(
                  init, scp, curr.name, res_slot, pure := FALSE);
                ProcessExecRes(JunoRT.ExecFromSlot(global_slot), vd);
                JunoRT.value_tbl[v.index] := JunoRT.value_tbl[res_slot]
              END;
              curr := curr.next
            END
          END
      ELSE (* SKIP *)
      END;
      forest := forest.next
    END
  END Pass3;
PROCEDURE Compile (
    te: T;
    time: VBT.TimeStamp;
    scp: JunoScope.T;
    VAR (*OUT*) nm: JunoAST.Id;
    VAR (*OUT*) entity: JunoScope.Mod;
    uniqueModName := TRUE): BOOLEAN =
  <* LL.sup < te *>
  BEGIN
    IF NOT Parse(te, time) THEN RETURN FALSE END;
    RETURN Compile2(te, time, scp, uniqueModName, nm, entity)
  END Compile;
PROCEDURE Compile2 (
    te: T;
    time: VBT.TimeStamp;
    parent: JunoScope.T;
    uniqueModName: BOOLEAN;
    VAR (*OUT*) nm: JunoAST.Id;
    VAR (*OUT*) entity: JunoScope.Mod)
  : BOOLEAN =
  <* LL.sup < te *>
  VAR forest := te.trees; restrict, public, scp: JunoScope.T; BEGIN
    TRY
      restrict := Pass0(forest, parent, uniqueModName, nm);
      (* Initialize "public", "scp" so module is compiled under restricted
         scope. *)
      public := JunoScope.New(restrict);
      scp := JunoScope.New(restrict);
      Pass1(forest, public, scp, nm);
      Pass2(forest, scp);
      Pass3(forest, scp);
      (* Make "parent" the parent scope of "public" and "scp" *)
      IF restrict # parent THEN
      	JunoScope.SetParent(public, parent);
      	JunoScope.SetParent(scp, parent)
      END
    EXCEPT
      Error (err) =>
        <* ASSERT err.ast # NIL *>
        te.textPretty := FALSE; (* for error to be unparsed *)
        Unparse(te, err.ast, err.msg, time);
        RETURN FALSE
    END;
    entity := NEW(JunoScope.Mod, public_scp := public, scp := scp);
    RETURN TRUE
  END Compile2;
TYPE InCnt = { EqualsZero, EqualsOne, AtLeastOne, Any };
PROCEDURE Pass4 (rt: View.Root; ed: T; scp: JunoScope.T) RAISES {Error} =
Compile and process UI declarations.
  VAR forest := ed.trees; BEGIN
    (* clear the "UI" tables *)
    EVAL NARROW(ed.toolTypes, AtomAtomTbl.Default).init(
      sizeHint := ed.toolTypes.size());
    EVAL NARROW(ed.setMenus, AtomRefTbl.Default).init(
      sizeHint := ed.setMenus.size());
    WHILE forest # NIL DO
      TYPECASE forest.tree OF
      | JunoAST.UIDecl (ui) =>
          IF ui.name = PointToolSym OR ui.name = TextToolSym
             OR ui.name = SetToolSym OR ui.name = TemplToolSym THEN
            VAR nm: JunoAST.QId; ent: JunoScope.Entity; BEGIN
              nm := FirstName(ui, argCnt := 1);
              ent := CheckEnt(nm, scp);
              IF ui.name = PointToolSym THEN
                IF NOT ISTYPE(ent, JunoScope.Code) THEN
                  (* not a predicate, function, or procedure *)
                  Raise("Must be a predicate, function, or procedure", nm)
                END;
                IF ISTYPE(ent, JunoScope.Proc) THEN
                  CheckProc(ent, nm)	 (* check for no OUT or INOUT args *)
                END
              ELSIF ui.name = TextToolSym THEN
                CheckProc(ent, nm, InCnt.AtLeastOne)
              ELSIF ui.name = SetToolSym THEN
                CheckProc(ent, nm, InCnt.EqualsOne)
              ELSIF ui.name = TemplToolSym THEN
                CheckProc(ent, nm, InCnt.EqualsZero)
              END;
              IF ed.toolTypes.put(nm.id1, ui.name) THEN
                Raise("Duplicate UI declaration", nm)
              END
            END
          ELSIF ui.name = ParamSym THEN
            VAR
              nm := FirstName(ui, argCnt := 2);
              ent := CheckEnt(nm, scp);
              valueAST := ui.args.head.next.expr;
              mod := ModuleName(ed);
              buttonName: TEXT;
              button: VBT.T;
              menu: VBT.T;
              menuRef: REFANY;
            BEGIN
              CheckProc(ent, nm, InCnt.EqualsOne);
              TYPECASE valueAST OF
                JunoAST.LitValue => (*SKIP*)
              | JunoAST.QId (qid) =>
                  (* Check that "qid" names a legal term *)
                  VAR res, unit: JunoScope.Entity; BEGIN
                    res := JunoScope.LookupQId(scp, qid, unit);
                    TYPECASE res OF
                      NULL =>
                        Raise("Unknown identifier", qid)
                    | JunoScope.Const, JunoScope.Var, JunoScope.Proc =>
                        (* SKIP - these are legal terms *)
                    ELSE
                        Raise("Parameter value must be\n"
                          & "a CONST, VAR, or PROC", qid)
                    END
                  END;
                  valueAST := Qualify(qid, mod)
              ELSE
                Raise("Parameter value must be a\n"
                  & "(qualified) identifier or literal", valueAST)
              END;
              <* FATAL Wr.Failure *>
              VAR twr := NEW(TextWr.T).init(); BEGIN
                JunoUnparse.Expr(twr, valueAST, tokens := LAST(INTEGER),
                  width := LAST(INTEGER), prec := JunoConfig.realPrec);
                buttonName := TextWr.ToText(twr)
              END;
              button :=
                NEW(ToolBox.SetButton).init(rt, buttonName,
                  Drawing.NewSetTool(Qualify(nm, mod), valueAST));
              IF NOT ed.setMenus.get(nm.id1, menuRef) THEN
                menu := NEW(HVSplit.T).init(Axis.T.Ver);
                EVAL ed.setMenus.put(nm.id1, menu)
              ELSE
                menu := menuRef
              END;
              Split.AddChild(menu, button)
            END
          ELSE
            Raise("Unknown UI declaration", ui)
          END
      ELSE (* SKIP *)
      END;
      forest := forest.next
    END
  END Pass4;
PROCEDURE Qualify (qid: JunoAST.QId; mod: Atom.T): JunoAST.QId =
Ifqidis unqualified andmod # NIL, returnmod . qid.id1, else returnqid.
  BEGIN
    IF qid.id0 = JunoAST.NilId AND mod # NIL
      THEN RETURN NEW(JunoAST.QId, bp := qid, id0 := mod, id1 := qid.id1)
      ELSE RETURN qid
    END
  END Qualify;
PROCEDURE FirstName (ui: JunoAST.UIDecl; argCnt: CARDINAL): JunoAST.QId
    RAISES {Error} =
Checks thatuihasargCntarguments, which is required to be non-zero. If so, returns the unqualified identifier that is the first argument. RaisesErrorwith the appropriate error message if the first argument is not an unqualified identifier.
  BEGIN
    <* ASSERT argCnt > 0 *>
    IF ui.args.size # argCnt THEN
      VAR errAST: JunoAST.T; BEGIN
        IF ui.args.size = 0 THEN errAST := ui ELSE errAST := ui.args END;
        Raise("Wrong number of arguments", errAST)
      END
    END;
    TYPECASE ui.args.head.expr OF
      JunoAST.QId (qid) =>
        IF qid.id0 # JunoAST.NilId THEN
          Raise("Expecting unqualified identifier", qid)
        END;
        RETURN qid
    ELSE
      Raise("Expecting an identifier", ui.args.head.expr);
      RETURN NIL (* not reached -- just to surpress compiler warning *)
    END
  END FirstName;
PROCEDURE CheckEnt (qid: JunoAST.QId; scp: JunoScope.T): JunoScope.Entity
    RAISES {Error} =
Returns the entity bound toqidinscp. Requires thatqidis unqualified. RaisesErrorwith an appropriate error message ifqidis not bound inscp.
  VAR res := JunoScope.Lookup(scp, qid.id1); BEGIN
    IF res = NIL THEN
      Raise("Undefined", qid)
    END;
    RETURN res
  END CheckEnt;
PROCEDURE CheckProc (ent: JunoScope.Entity; ast: JunoAST.T; inCnt := InCnt.Any)
    RAISES {Error} =
Check thatentis a procedure with no OUT or INOUT parameters. IfinCntisEqualsOne, then the procedure must have exactly one IN argument; if it isAtLeastOne, then it must have at least one IN argument. RaisesErrorso thatastwill be highlighted if any of these checks fail; otherwise, this procedure is a no-op.
  BEGIN
    TYPECASE ent OF JunoScope.Proc (p) =>
      IF p.out_cnt # 0 OR p.inout_cnt # 0 THEN
        Raise("Procedure may not have any\nOUT or INOUT arguments", ast)
      END;
      IF inCnt = InCnt.EqualsOne AND p.in_cnt # 1 THEN
        Raise("Procedure must have\nexactly one IN argument", ast)
      ELSIF inCnt = InCnt.AtLeastOne AND p.in_cnt = 0 THEN
        Raise("Procedure must have\nat least one IN argument", ast)
      ELSIF inCnt = InCnt.EqualsZero AND p.in_cnt # 0 THEN
        Raise("Procedure must have\nno IN arguments", ast)
      END
    ELSE Raise("Must be a procedure", ast)
    END
  END CheckProc;
PROCEDURE CompileUI (
    rt: View.Root;
    te: T;
    time: VBT.TimeStamp;
    scp: JunoScope.T): BOOLEAN =
BEGIN
    <* ASSERT te.treesValid *>
    TRY
      Pass4(rt, te, scp)
    EXCEPT
      Error (err) =>
        <* ASSERT err.ast # NIL *>
        Unparse(te, err.ast, err.msg, time);
        RETURN FALSE
    END;
    RETURN TRUE
END CompileUI;
PROCEDURE SaveSlots (wr: Wr.T) =
  BEGIN
    Wr.PutText(wr, Fmt.Int(global_slot) & "\n")
  END SaveSlots;
PROCEDURE RestoreSlots (rd: Rd.T) =
  <* FATAL FloatMode.Trap, Lex.Error, Rd.Failure, Rd.EndOfFile *>
  BEGIN
    global_slot := Lex.Int(rd);
    IF Rd.GetChar(rd) # 'n' THEN <* ASSERT FALSE *> END
  END RestoreSlots;
BEGIN
  PointToolSym := Atom.FromText("PointTool");
  TextToolSym  := Atom.FromText("TextTool");
  SetToolSym   := Atom.FromText("SetTool");
  ParamSym     := Atom.FromText("Param");
  TemplToolSym := Atom.FromText("Template");
END Editor.