-- SMPrettyImpl.mesa, module to output pretty text from a parse tree -- last edit by Russ Atkinson, 16-Dec-81 14:36:33 -- last edit by Schmidt, May 16, 1983 4:52 pm -- last edit by Satterthwaite, August 11, 1983 2:20 pm -- The following comment is useful to measure indentation. -- 1 2 3 4 5 6 7 8 9 -- 456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 DIRECTORY Atom: TYPE USING [GetPName], CS: TYPE USING [RopeFromStamp], IO: TYPE USING [card, PutChar, PutF, PutFR, rope, STREAM], Rope: TYPE USING [Flatten, Text], SMCommentTable: TYPE USING [Ref], SMCommentTableOps: TYPE USING [CommentM, Explode, FindNext, TestBreakHint], SMFI: TYPE USING [BcdFileInfo, SrcFileInfo], SMUtil: TYPE USING [], SMTree: TYPE Tree USING [ Handle, Id, Link, Name, NodeName, Text, null, nullHandle, nullId, nullName], SMTreeOps: TYPE --TreeOps-- USING [GetName, NSons, NthSon, OpName]; SMPrettyImpl: CEDAR PROGRAM IMPORTS Atom, CS, IO, Rope, SMCommentTableOps, SMTreeOps EXPORTS SMUtil SHARES Rope ~ { OPEN Tree~~SMTree, TreeOps~~SMTreeOps; Index: TYPE ~ INT; InvalidIndex: Index ~ Index.LAST; PPS: TYPE~REF PPState; PPState: TYPE~RECORD[ -- global information used by the pretty printer out: IO.STREAM←, -- the output stream indent: NAT←0, position: NAT←0, line: INT←0, sizing: BOOL←FALSE, lastChar: CHAR←'\000, lateTrigger: NAT←80, earlyTrigger: NAT←64, -- options smallSons: NAT←2, -- this stuff is the state information comments: SMCommentTableOps.CommentM←, -- comment table lastIndex: Index←Index.LAST, nextIndex: Index←0]; Excess: ERROR ~ CODE; GetInfo: PROC[t: Tree.Link] RETURNS[info: Index] ~ { WITH t SELECT FROM x: Tree.Handle => { info ← x.info; IF info = 0 THEN FOR i: NAT IN [1..TreeOps.NSons[t]] WHILE info = 0 DO info ← GetInfo[TreeOps.NthSon[t, i]] ENDLOOP} ENDCASE => info ← 0}; WriteChar: PROC[pps: PPS, c: CHAR] ~ { IF (pps.lastChar ← c) < '\040 THEN { SELECT c FROM '\t => { pps.position ← pps.position + 8; pps.position ← pps.position - pps.position MOD 8; pps.lastChar ← ' }; '\n, '\f => {pps.position ← 0; pps.line ← pps.line + 1}; ENDCASE} ELSE pps.position ← pps.position + 1; IF pps.sizing THEN { IF pps.lastChar # ' AND pps.position >= pps.lateTrigger THEN ERROR Excess} ELSE pps.out.PutChar[c]}; WriteName: PROC[pps: PPS, name: Tree.Name] ~ { r: Rope.Text ~ Atom.GetPName[name]; len: CARDINAL ~ r.length; pps.position ← pps.position + len; pps.lastChar ← 'A; IF pps.sizing THEN { IF pps.position >= pps.lateTrigger THEN ERROR Excess} ELSE FOR i: CARDINAL IN [0..len) DO pps.out.PutChar[r[i]] ENDLOOP; }; WriteToken: PROC[pps: PPS, r: Rope.Text] ~ { len: CARDINAL ~ r.length; IF len # 0 THEN { pps.position ← pps.position + len; pps.lastChar ← r[len-1]; IF pps.sizing THEN { IF pps.position >= pps.lateTrigger THEN ERROR Excess} ELSE FOR i: CARDINAL IN [0..len) DO pps.out.PutChar[r[i]] ENDLOOP; }; }; WriteText: PROC[pps: PPS, r: Rope.Text] ~ { IF r # NIL THEN FOR i: CARDINAL IN [0..r.length) DO WriteChar[pps, r[i]] ENDLOOP; }; WriteQuotedText: PROC[pps: PPS, r: Rope.Text] ~ { -- we assume that the scanner has left escape marks -- but that the leading and trailing quotes are gone WriteChar[pps, '"]; IF r # NIL THEN { FOR i: NAT IN [0..r.length) DO WriteChar[pps, r[i]] ENDLOOP}; WriteChar[pps, '"]}; Indent: PROC[pps: PPS] ~ { THROUGH [pps.position .. pps.indent) DO WriteChar[pps, ' ] ENDLOOP}; Break: PROC[pps: PPS] ~ { IF pps.position > pps.indent THEN WriteChar[pps, '\n]; Indent[pps]}; -- sizing procedures WillExceed: PROC[ pps: PPS, t: Tree.Link, pattern: Rope.Text, level, pos: NAT, lineExceed: BOOL←FALSE] RETURNS[exceed: BOOL ← FALSE] ~ { -- First, save the entire state oldLastChar: CHAR ~ pps.lastChar; oldLastIndex: Index ~ pps.lastIndex; oldNextIndex: Index ~ pps.nextIndex; oldIndent: NAT ~ pps.indent; oldPosition: NAT ~ pps.position; oldLine: INT ~ pps.line; IF pps.sizing THEN RETURN[FALSE]; -- now, try to determine the size of things pps.sizing ← TRUE; [] ← PrettyPattern[pps, t, pattern, level, pos ! Excess => {exceed ← TRUE; CONTINUE}]; IF lineExceed AND pps.line > oldLine THEN exceed ← TRUE; -- last, restore the entire state pps.lastChar ← oldLastChar; pps.lastIndex ← oldLastIndex; pps.nextIndex ← oldNextIndex; pps.indent ← oldIndent; pps.position ← oldPosition; pps.line ← oldLine; pps.sizing ← FALSE}; FlushCommentsBefore: PROC[pps: PPS, index: Index] ~ { IF pps.comments # NIL THEN { comment: SMCommentTable.Ref ← NIL; WHILE index >= pps.nextIndex DO oldLine: Index ~ pps.line; comment ← (pps.comments).FindNext[pps.nextIndex]; IF comment = NIL THEN EXIT ELSE { cIndex, lastToken, prefix: Index; text: Rope.Text; [cIndex, text, lastToken, prefix] ← SMCommentTableOps.Explode[comment]; pps.nextIndex ← cIndex; IF pps.nextIndex > index THEN EXIT; pps.nextIndex ← pps.nextIndex + 1; IF ~(pps.sizing OR index = InvalidIndex) THEN IF prefix > 0 OR pps.position + text.length > pps.lateTrigger THEN Break[pps]; IF text = NIL OR text.length < 2 THEN WriteChar[pps, '\n] ELSE IF pps.position > pps.indent AND pps.lastChar # ' THEN WriteChar[pps, ' ]; FOR i: Index IN [2..prefix] DO WriteChar[pps, '\n] ENDLOOP; Indent[pps]; WriteText[pps, text]}; IF pps.line = oldLine THEN WriteChar[pps, ' ]; Indent[pps] ENDLOOP }; }; PrettyPrint: PUBLIC PROC[ out: IO.STREAM, root: Tree.Link, comments: SMCommentTableOps.CommentM] ~ { IF root # Tree.null THEN { next: SMCommentTable.Ref ~ (IF comments=NIL THEN NIL ELSE comments.FindNext[0]); pps: PPS ~ NEW[PPState ← [ out~out, comments~comments, nextIndex~(IF next=NIL THEN InvalidIndex ELSE SMCommentTableOps.Explode[next].start), lastIndex~InvalidIndex, lastChar~' ]]; PrettyLink[pps, root, 0]; FlushCommentsBefore[pps, InvalidIndex]; WriteChar[pps, '\n]}; }; PrettyLink: PROC[pps: PPS, t: Tree.Link, info: Index, level: NAT←0] ~ { IF t # Tree.null THEN { SELECT pps.position FROM < pps.indent => Indent[pps]; > pps.lateTrigger => IF ~pps.sizing THEN Break[pps] ENDCASE; WITH t SELECT FROM name: Tree.Name => PrettyName[pps, name, info]; id: Tree.Id => PrettyId[pps, id]; text: Tree.Text => PrettyText[pps, text, info]; node: Tree.Handle => PrettyNode[pps, node, level]; fiSrc: SMFI.SrcFileInfo => WriteToken[pps, IO.PutFR["@%s!%d", IO.rope[fiSrc.localName], IO.card[fiSrc.create]].Flatten[]]; fiBcd: SMFI.BcdFileInfo => WriteToken[pps, IO.PutFR["@%s!%s", IO.rope[fiBcd.localName], IO.rope[CS.RopeFromStamp[fiBcd.stamp]]].Flatten[]]; ENDCASE => ERROR; }; }; PrettyName: PROC[pps: PPS, name: Tree.Name, info: Index] ~ { IF name = Tree.nullName THEN WriteToken[pps, "(anon)"] ELSE {FlushCommentsBefore[pps, info]; WriteName[pps, name]}}; PrettyId: PROC[pps: PPS, id: Tree.Id] ~ INLINE { IF id = Tree.nullId THEN WriteToken[pps, "(anon)"] ELSE { -- cf. SMValImpl.IdName d: Tree.Handle ~ (IF id.db.name = $decl THEN id.db ELSE NARROW[id.db[1]]); WriteName[pps, TreeOps.GetName[TreeOps.NthSon[d.son[id.p], 1]]]}; }; PrettyText: PROC[pps: PPS, text: Tree.Text, info: Index] ~ { FlushCommentsBefore[pps, info]; WriteQuotedText[pps, text]}; OpKind: TYPE ~ {binary, pattern, special}; PrettyNode: PROC[pps: PPS, node: Tree.Handle, level: NAT←0] ~ { oldIndent: Index ~ pps.indent; IF node = Tree.nullHandle THEN NULL ELSE IF node.name = $locator THEN PrettyLink[pps, node.son[1], node.info, level] ELSE { text: Rope.Text; kind: OpKind; newLevel: NAT; info: Index ~ GetInfo[node]; IF info # 0 THEN FlushCommentsBefore[pps, info]; [text, newLevel, kind] ← LookAtNode[node]; IF newLevel < level THEN {WriteChar[pps, '(]; pps.indent ← pps.position}; SELECT kind FROM $special => PrettySpecial[pps, node]; $binary => { left: Tree.Link ~ TreeOps.NthSon[node, 1]; right: Tree.Link ~ TreeOps.NthSon[node, 2]; [] ← PrettyPattern[pps, left, "%0 ", newLevel]; WriteToken[pps, text]; [] ← PrettyPattern[pps, right, "%+2%|%0", newLevel + 1]}; $pattern => [] ← PrettyPattern[pps, node, text, newLevel] ENDCASE => ERROR; IF newLevel < level THEN WriteChar[pps, ')]}; pps.indent ← oldIndent}; LookAtNode: PROC[node: Tree.Handle] RETURNS[text: Rope.Text, level: NAT, kind: OpKind] ~ { nSons: CARDINAL ~ TreeOps.NSons[node]; level ← 0; text ← NIL; kind ← $pattern; SELECT node.name FROM $lambda => text ← "LAMBDA %(%+4%1%) => %2 IN %(%+2%3%)"; $let => text ← "LET %1 IN %(%+2%2%)"; $arrow => text ← "%1 -> %2"; $apply => text ← "%1%2"; $applyDefault => text ← "%1*%2"; $subscript => text ← "%1.%2"; $union => text ← "%1 %|+ %2"; $then => text ← "%1 %|THEN %2"; $exclusion => text ← "%1 - %2"; $restriction => text ← "%1↑%2"; $splitUpper => text ← "%1\\%2"; $splitLower => text ← "%1/%2"; $cross => text ← "%1 CROSS %2"; $cross2 => text ← "%1 CROSS CROSS %2"; $group => text ← "[%,0]"; $bind => text ← "[%,0]"; $bindRec => text ← "REC [%;0]"; $bindElem => text ← "%|%1~%2"; $nBind => text ← "[%1~%2]"; $nBindRec => text ← "REC [%1~%2]"; $decl => text ← (SELECT TRUE FROM (nSons = 0) => "[]", (nSons = 1 AND ~node.attrs[1]) => "%1", ENDCASE => "[%,0]"); $declElem => text ← (IF node.son[2]= Tree.null THEN "%|%1" ELSE "%|%1: %2"); -- $type => text ← "TYPE%(0%?1 %0%)"; $type => text ← (IF node.son[1]=Tree.null THEN "TYPE" ELSE "TYPE %1"); $typeSTRING => text ← "STRING"; $control => text ← "CONTROL"; $env => text ← "ENV"; $nil => text ← "NIL"; $unitId => kind ← $special; -- old: text ← "@%1%2%[.3%0%]" -- $uiPart => text ← "%(%B*%)%1%(%A↑%)"; ENDCASE => ERROR; }; PrettySpecial: PROC[pps: PPS, t: Tree.Link] ~ { oldIndent: Index ~ pps.indent; SELECT TreeOps.OpName[t] FROM $unitId => { son1: Tree.Link ~ TreeOps.NthSon[t, 1]; son2: Tree.Link ~ TreeOps.NthSon[t, 2]; son3: Tree.Link ~ TreeOps.NthSon[t, 3]; son4: Tree.Link ~ TreeOps.NthSon[t, 4]; WriteChar[pps, '@]; IF son1 ~= Tree.null THEN { WriteChar[pps, '[]; WriteToken[pps, NARROW[son1]]; WriteChar[pps, ']]}; IF son2 ~= Tree.null THEN { WriteChar[pps, '<]; FOR i: NAT IN [1 .. TreeOps.NSons[son2]] DO WriteToken[pps, NARROW[TreeOps.NthSon[son2, i]]]; WriteChar[pps, '>]; ENDLOOP; }; FOR i: NAT IN [1 .. TreeOps.NSons[son3]] DO t: Tree.Link ~ TreeOps.NthSon[son3, i]; IF i # 1 THEN WriteChar[pps, '.]; WITH t SELECT FROM part: Rope.Text => WriteToken[pps, part]; node: Tree.Link => { PrettyLink[pps, TreeOps.NthSon[node, 1], 0]; WriteChar[pps, '↑]}; ENDCASE; ENDLOOP; IF son4 ~= Tree.null THEN { WriteChar[pps, '!]; WriteToken[pps, NARROW[son4]]}; }; ENDCASE => ERROR; pps.indent ← oldIndent}; PrettyList: PROC[pps: PPS, t: Tree.Link, separator: CHAR←';] ~ PrettySons; PrettyBrackets: PROC[pps: PPS, t: Tree.Link, left: CHAR←'\000, separator: CHAR←';] ~ { oldIndent: Index ← pps.indent; IF left # '\000 THEN WriteChar[pps, left]; pps.indent ← pps.position; PrettyList[pps, t, separator]; SELECT left FROM '{ => WriteChar[pps, '}]; '( => WriteChar[pps, ')]; '[ => WriteChar[pps, ']] ENDCASE; pps.indent ← oldIndent}; PrettySons: PROC[pps: PPS, t: Tree.Link, separator: CHAR←';] ~ { WITH t SELECT FROM node: Tree.Handle => { sons: NAT ~ TreeOps.NSons[node]; break: BOOL ← (separator = '; AND sons > pps.smallSons); IF separator = '. THEN {break ← TRUE; separator ← ',}; IF separator = ': THEN {break ← FALSE; separator ← ';}; IF sons # 0 THEN { son1: Tree.Link ~ node.son[1]; lastInfo: Index ← GetInfo[son1]; lastLine: INT ← pps.line; FlushCommentsBefore[pps, lastInfo]; IF break THEN Break[pps]; lastLine ← pps.line; PrettyLink[pps, son1, lastInfo]; FOR i: NAT IN [2..sons] DO link: Tree.Link ~ node.son[i]; thisInfo: Index ~ GetInfo[link]; thisBreak: BOOL ← -- lots of ways to break here break OR pps.line # lastLine OR pps.position > pps.earlyTrigger AND sons - i > 1 OR separator = '; AND (pps.comments # NIL AND (pps.comments).TestBreakHint[lastInfo, thisInfo]) OR pps.position > pps.indent AND WillExceed[pps, link, ",%@ %0", 0, 0] -- OR (separator = ', AND BreakHintFromNode[link]) --; IF i = 2 AND ~break AND thisInfo > lastInfo AND lastInfo > 0 AND (pps.comments # NIL AND (pps.comments).TestBreakHint[lastInfo, thisInfo]) THEN -- HINT: first two items in list are on separate lines break ← thisBreak ← TRUE; IF link = Tree.null AND separator = '; THEN LOOP; WriteChar[pps, separator]; WriteChar[pps, ' ]; FlushCommentsBefore[pps, lastInfo ← thisInfo]; IF thisBreak THEN Break[pps]; lastLine ← pps.line; PrettyLink[pps, link, thisInfo] ENDLOOP}; } ENDCASE; }; PrettyPattern: PROC[ pps: PPS, t: Tree.Link, pattern: Rope.Text, level: NAT←0, pos: NAT←0, enable: BOOL←TRUE] RETURNS[NAT] ~ { -- This procedure takes care of expanding formatting patterns -- The '%' character is used to denote the start of an expansion. -- The characters following the % character are decoded specially, as follows: -- (the following options are interpreted regardless of the enable flag) -- (n: start a new recursion level, saving context, using son n -- [n: start iteration over son n (wants separator character before n) -- ): return from current level, should pair with '( -- ]: end of iteration, should pair with '[ -- (the following options are interpreted only when enable is true) -- 0..9: expand the Nth son (where TreeOps.NthSon[t, 0] = t) -- ,n: expand list using son n, ', separator, default no break -- ;n: expand list using son n, '; separator, default break -- .n: expand list using son n, ', separator, default break -- :n: expand list using son n, '; separator, default no break -- ?n: enable ← Nth son # NIL -- ~?n: enable ← Nth son = NIL -- |: break if remainder of pattern will exceed margin -- *: same as %+2%|%@ -- ↑n: break if break hint between t and son n, or remainder exceeds margin -- +n: pps.indent ← MIN[original indent + n, position] -- /n: flush comments before son n, then break -- @: indent ← position -- !: exit entire pattern -- all other characters following % are written literally size: Index ~ (IF pattern = NIL THEN 0 ELSE pattern.length); GetSon: PROC RETURNS[Tree.Link] ~ { n: NAT ~ GetNum[]; RETURN [SELECT n FROM 0 => t, > nSons => Tree.null, ENDCASE => TreeOps.NthSon[t, n]] }; GetChar: PROC RETURNS[c: CHAR] ~ INLINE { IF pos >= size THEN RETURN['%]; c ← pattern[pos]; pos ← pos + 1}; GetNum: PROC RETURNS[NAT] ~ INLINE { IF pos >= size THEN RETURN[0] ELSE { nc: CHAR ~ pattern[pos]; IF nc IN ['0..'9] THEN {pos ← pos + 1; RETURN[nc.ORD - '0.ORD]}; RETURN[0]}; }; nSons: NAT ← 0; oldIndent: NAT ~ pps.indent; oldLevel: NAT ~ level; oldLine: INT ~ pps.line; WITH t SELECT FROM node: Tree.Handle => nSons ← node.sonLimit-1; ENDCASE; WHILE pos < size DO c: CHAR ← GetChar[]; IF c # '% THEN {IF enable THEN WriteChar[pps, c]; LOOP}; SELECT (c ← GetChar[]) FROM '), '] => EXIT; '( => {pos ← PrettyPattern[pps, GetSon[], pattern, level, pos, enable]; LOOP}; '[ => {-- looping construct term: CHAR ~ GetChar[]; link: Tree.Link ← GetSon[]; lenb: BOOL ~ (enable AND link # Tree.null); IF lenb --AND TreeOps.OpName[link] = $list-- THEN { node: Tree.Handle ~ NARROW[link]; nls: NAT ~ node.sonLimit - 1; FOR i: NAT IN [1..nls - 1] DO [] ← PrettyPattern[pps, node[i], pattern, 0, pos]; WriteChar[pps, term] ENDLOOP; link ← node[nls]}; pos ← PrettyPattern[pps, link, pattern, 0, pos, lenb]; LOOP} ENDCASE; IF ~enable THEN LOOP; SELECT c FROM '0, '1, '2, '3, '4, '5, '6, '7, '8, '9 => { pos ← pos - 1; PrettyLink[pps, GetSon[], 0, level]}; '?, '~ => { IF c = '~ THEN {c ← GetChar[]; enable ← FALSE}; SELECT c FROM '? => IF GetSon[] = Tree.null THEN enable ← ~enable; ENDCASE => enable ← FALSE}; ';, ',, '., ': => PrettyList[pps, GetSon[], c]; '| => IF pps.position > pps.indent AND WillExceed[pps, t, pattern, level, pos, TRUE] THEN Break[pps]; '* => { pps.indent ← MIN[oldIndent + 2, pps.position]; IF pps.position > pps.indent AND WillExceed[pps, t, pattern, level, pos, TRUE] THEN Break[pps]; pps.indent ← pps.position}; '↑ => IF pps.position > pps.indent THEN { link: Tree.Link ← GetSon[]; IF link # Tree.null THEN { start: Index ~ GetInfo[t]; next: Index ~ GetInfo[link]; IF pps.position > pps.earlyTrigger OR (start # 0 AND next > start AND (pps.comments # NIL AND (pps.comments).TestBreakHint[start, next])) OR WillExceed[pps, t, pattern, level, pos, TRUE] THEN { FlushCommentsBefore[pps, next]; Break[pps]}}}; '/, '= => {FlushCommentsBefore[pps, GetInfo[GetSon[]]]; IF c = '/ THEN Break[pps]}; '$ => level ← GetNum[]; '! => {pos ← pos - 2; EXIT}; '@ => pps.indent ← MAX[pps.position, pps.indent]; '+ => pps.indent ← MIN[oldIndent + GetNum[], pps.position]; ENDCASE => WriteChar[pps, c] ENDLOOP; pps.indent ← oldIndent; level ← oldLevel; RETURN[pos]}; }.