module: ppml Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND // // PPML // // Program notes need to be stored and later displayed in browsers. This // presents us with two problems. At the point of creation we have no way of // knowing the column width that will be used when the note is displayed. // There may even be more than one width if we want to be smart when a browser // window is resized. A second problem arises if we store a program note in // the form of a condition string + arguments and the arguments are context // sensitive. We could just save everything as a string, but then the logical // structure of the message is lost. An alternative is to store the text in // a more structured form. This is the purpose of the class and its // derivatives. The interface is based on Oppen's 1980 TOPLAS paper. define constant = limited(, min: 0); define abstract class () end class ; define constant = ; // TODO: LIMITED SEQUENCES // = limited(, of: ); // The token-size method computes the maximum size of the ppml token, // i.e. assuming we aren't force to break it. define generic token-size (t :: ) => (token-size :: ); // // The simplest ppml token is just a string. define class () constant slot the-string :: , required-init-keyword: string:; end class ; define function ppml-string (str :: ) => (ppml :: ) make(, string: str) end; define method token-size (t :: ) => (s :: ); t.the-string.size end; define class () end class ; define function ppml-quote-string (str :: ) => (ppml :: ) make(, string: str) end; define method token-size (t :: ) => (s :: ); next-method() + 2 end; // // A indicates a position in the output where it is permissible // to break the output if it won't fit on a single line. If we don't // need to break the line then we output blank-space spaces. If we do need // to break then we indent offset spaces relative to the current line indent. define class () constant slot blank-space :: , init-value: 1, init-keyword: blank-space:; // number of spaces per blank constant slot offset :: , init-value: 0, init-keyword: offset:; // indent for overflow lines end class ; // Breaks occur frequently, so we cache the more common ones to save storage. define constant $break-cache-max-space = 3; define constant $break-cache-max-offset = 3; define constant $break-cache = make(, dimensions: vector($break-cache-max-space + 1, $break-cache-max-offset + 1)); for (i from 0 to $break-cache-max-space) for (j from 0 to $break-cache-max-offset) $break-cache[i,j] := make(, blank-space: i, offset: j) end end; define function ppml-break (#key space :: = 1, offset :: = 0) => (ppml :: ) if ( space >= 0 & space <= $break-cache-max-space & offset >= 0 & offset <= $break-cache-max-offset ) $break-cache[space, offset] else make(, blank-space: space, offset: offset) end if end; define method token-size (t :: ) => (s :: ); t.blank-space end; // We can force a line break by making a break with a space larger than the // column width. define constant $line-break = ppml-break(space: 999); // // To add structure to the output, we can package up a sequence of tokens // into a block. There are a couple of attributes associated with a block. // The offset indicates how much to indent subsequent lines of the block // if a break is necessary. When a block is longer than a line then we have // a number of options. We can display as much on each line as possible, // only breaking when really necessary. Alternatively, we can break the // block at each break point, e.g. // // aaa aaa bbb // bbb ccc ddd // ccc // ddd // // The choice of layout depends on whethe the break-type attribute of the // block is #"consistent" or #"inconsistent". The third alternative is // #"fit". This suppresses all breaks and truncates the output if it // won't fit on a line. // // The size of the block is cached in the block for efficiency. define constant = one-of(#"consistent", #"inconsistent", #"fit"); define class () constant slot constituent-tokens :: , required-init-keyword: constituents:; constant slot offset :: , init-value: 2, init-keyword: offset:; // indent for this group constant slot break-type :: , init-value: #"inconsistent", init-keyword: break-type:; slot cached-total-size :: false-or(), init-value: #f; end class ; define generic total-size (bt :: ) => (sz :: ); define method token-size (t :: ) => (s :: ); t.total-size end; define method total-size (bt :: ) => (sz :: ) bt.cached-total-size | (bt.cached-total-size := compute-block-size(bt.constituent-tokens)) end method total-size; define method compute-block-size (cs :: ) => (sz :: ) let prev-max = 0; let bs = 0; for (c in cs) if (c == $line-break) prev-max := max(prev-max, bs); bs := c.offset; else bs := bs + c.token-size end if end for; max(bs, prev-max) end method compute-block-size; define function ppml-block (constituents :: , #key offset :: = 2, type :: = #"inconsistent") => (ppml :: ) make(, constituents: constituents, offset: offset, break-type: type) end; // // When constructing blocks representing collections it is wasteful to // explicitly store the separators between elements. The // class captures this common case. define constant $comma-ppml-separator = vector(ppml-string(","), ppml-break(space: 1)); define class () constant slot separator :: , init-value: $comma-ppml-separator, init-keyword: separator:; end class ; define method token-size (t :: ) => (s :: ); let csize = t.constituent-tokens.size; if (csize = 0) 0 else // TODO: This produces the wrong answer if the block contains $line-break let block-size = t.total-size; let separator-size = compute-block-size(t.separator); block-size + ((csize - 1) * separator-size); end; end; define function ppml-separator-block (constituents :: , #key separator :: = $comma-ppml-separator, offset :: = 0, type :: = #"inconsistent", left-bracket :: false-or() = #f, right-bracket :: false-or() = #f) => (ppml :: ) let body = make(, constituents: constituents, separator: separator, offset: offset, break-type: type); if (left-bracket | right-bracket) let v = vector(left-bracket, body, right-bracket); let left-bracket-size = 0; if (left-bracket) left-bracket-size := left-bracket.token-size; else v[0] := ppml-string("") end; unless (right-bracket) v[2] := ppml-string("") end; ppml-block(v, offset: left-bracket-size); else body end; end; // // Sometimes it is more space efficient to delay the construction of the // equivalent of an object until we need to print it. The // class supports this. It contains either a // token, or a pair of a function and its arguments. When we need a token // and encounter the pair we apply the function to its arguments. This // should return an instance of . Optionally we can overwrite the // pair by the result. define class () slot the-token, // :: one-of(, ), required-init-keyword: pair:; constant slot cache-token? :: , init-keyword: cache-token?:, init-value: #t; end class ; define function ppml-suspension (fun :: , #rest args) => (ppml :: ) make(, pair: pair(fun,args)) end; define method suspension-token (t :: ) => (st :: ); let tok = t.the-token; if (tok.object-class == ) let real-token = apply(tok.head, tok.tail); if (t.cache-token?) t.the-token := real-token else real-token end else tok end end; define method token-size (t :: ) => (s :: ); suspension-token(t).token-size end; // // The browser "knows" about some of the objects manipulated by the compiler, // e.g. the various kinds of definition, and so we store these directly. // Furthermore we recompute the ppml representation of the object // every time token-size is called as the representation may depend on // browser settings. define class () constant slot the-object :: , required-init-keyword: object:; slot ppml-equivalent :: , init-value: ppml-string(""); end class ; define function ppml-browser-aware-object (o :: ) => (ppml :: ) make(, object: o) end; define method token-size (t :: ) => (s :: ); token-size(t.ppml-equivalent := ppml-string(format-to-string("%=", t.the-object))) end; // // AS // // To make it easier to construct ppml terms, we add methods to AS. // The default is to just convert the object into a string, and then // apply ppml-string. // define method as (class == , o :: ) => (instance :: ); ppml-string(format-to-string("%=", o)) end; // & define method as (class == , string :: ) => (instance :: ); ppml-quote-string(string) end; define method as (class == , symbol :: ) => (instance :: ); ppml-string(as(, symbol)) end; // define method as (class == , ppml :: ) => (instance :: ); ppml end; // // In the collection cases we try to share as many tokens as possible to // reduce the space overhead of ppml. define function as-ppml (o) => (po :: ) as(, o) end; define constant ppml-sb-template = vector(ppml-string("#["), ppml-string("..."), ppml-string("]")); define constant ppml-rb-template = vector(ppml-string("#("), ppml-string("..."), ppml-string(")")); define method as (class == , collection :: ) => (instance :: ); let v = ppml-sb-template.shallow-copy; v[1] := ppml-separator-block(map-as(, as-ppml, collection), offset: 0); ppml-block(v, offset: 2); end method as; // define constant ppml-table-element-template = vector(ppml-string("("), ppml-string("key"), ppml-string(" ->"), ppml-break(space: 1, offset: 1), ppml-string("value"), ppml-string(")")); define method as (class == , collection :: ) => (instance :: ); let v = ppml-sb-template.shallow-copy; let cv = make(, size: collection.size); let (initial-state, limit, next-state, finished-state?, current-key, current-element) = collection.forward-iteration-protocol; for (state = initial-state then next-state(collection, state), i from 0, until: finished-state?(collection, state, limit)) let te = ppml-table-element-template.shallow-copy; te[1] := as(, current-key(collection, state)); te[4] := as(, current-element(collection, state)); cv[i] := ppml-block(te, offset: 1); end for; v[1] := ppml-separator-block(cv, offset: 0); ppml-block(v, offset: 2); end method as; // define method as (class == , vec :: ) => (instance :: ); let v = ppml-sb-template.shallow-copy; v[1] := ppml-separator-block(map-as(, as-ppml, vec), offset: 0); ppml-block(v, offset: 2); end method as; // define method as (class == , list :: ) => (instance :: ); let v = ppml-rb-template.shallow-copy; let constituents = make(); // TODO: Use a here. let remainder = #(); for (ptr = list then ptr.tail, until: ~instance?(ptr, ) | ptr.empty?) push-last(constituents, as(, ptr.head)); finally remainder := ptr end for; if (remainder == #()) v[1] := ppml-separator-block(as(, constituents), offset: 0) else v[1] := ppml-block(vector( ppml-separator-block(as(, constituents), offset: 0), ppml-break(space: 1, offset: 1), ppml-string(". "), as(, remainder)), offset: 0) end; ppml-block(v, offset: 2); end method as; // // FORMAT-TO-PPML : A PPML equivalent of format-to-string // // We insert breaks at the places where arguments are inserted in the // format string. This will hopefully give us reasonable output, but not // always as good as we could do by hand. We separate out the processing // of the format string so that we can share the constant components of the // resulting ppml-block if the same format expression is used multiple // times. define thread variable *verbose-condition-output* = #f; define method ppml-format-string (string :: ) => (f :: ) let arguments = make(); let constituents = make(); let limit = string.size; let i = 0; while (i < limit) let character = ' '; let start-index = i; while ( i < limit & (character := string[i]) ~= '\n' & character ~= '%') i := i + 1 end; if (i > start-index) for (j = i - 1 then j - 1, until: j = start-index | string[j] ~= ' ') finally unless (string[j] = ' ') push-last(constituents, ppml-string(copy-sequence(string, start: start-index, end: j + 1))); end; push-last(constituents, ppml-break(space: i - j - 1)); end; end; if (i < limit) if (character == '%') if (i + 1 = limit) error("Invalid format string: '%s'", string) end; let format-char = as-uppercase(string[i + 1]); select (format-char) 'X', 'D', 'S', 'C', '=' => push-last(arguments, pair(constituents.size, format-char)); push-last(constituents, #f); '%' => push-last(constituents, ppml-string("%")); '\n', '&' => push-last(constituents, ppml-string("\n")); otherwise => error("Invalid '%s' command '%s'", '%', string[i + 1]) end select; i := i + 2; for (j = i then j + 1, while: j < limit & string[j] = ' ') finally unless (i = j) push-last(constituents, ppml-break(space: j - i)); i := j end end else // '\n' case push-last(constituents, $line-break); i := i + 1; end end if end while; let constituents = as(, constituents); let arguments = as(, arguments); method (#rest args) let b = shallow-copy(constituents); for (arg-descriptor in arguments, arg in args) let index = arg-descriptor.head; case *verbose-condition-output* => b[index] := ppml-string(format-to-string("%=", arg)); arg-descriptor.tail == 'S' => case instance?(arg, ) => b[index] := ppml-string(arg); instance?(arg, ) => b[index] := ppml-string(arg.the-string); otherwise => b[index] := as(, arg) end; otherwise => b[index] := as(, arg) end; end; ppml-block(b, offset: 0) end; end method ppml-format-string; define method format-to-ppml (string :: , #rest args) => (ppml :: ) apply(ppml-format-string(string), args) end method format-to-ppml; // // PPML Printer // // When outputing ppml we need to keep track of the space left on the // current line and the current margin. We store these values in a // object, along with the functions used to display text // and line breaks. define class () constant slot margin :: , required-init-keyword: margin:; slot space :: , init-value: 0; slot terse-depth :: , init-value: 100, init-keyword: terse-depth:; constant slot ppml-output-string = method (s :: ) write(*standard-output*, s) end, init-keyword: output-function:; constant slot ppml-newline = method () write(*standard-output*, "\n") end, init-keyword: newline-function:; end class ; define method initialize (pp :: , #next next-method, #key) next-method(); pp.space := pp.margin; end; define method blanks (pp :: , n :: ) for (i from 0 below n) pp.ppml-output-string(" ") end; pp.space := pp.space - n; end method; // // PRINT-PPML // define generic print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); define generic print-ppml-block (t :: , pp :: , btype :: , block-space :: , after :: ); define method print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); if (t.the-string.size <= pp.space) pp.ppml-output-string(t.the-string); pp.space := pp.space - t.the-string.size; #t else // The string is too long... let str = t.the-string; let break-point = pp.space; for (i from pp.space - 1 to 0 by -1, until: str[i] = ' ') finally if (i > 0) break-point := i + 1 end end; pp.ppml-output-string(copy-sequence(str, start: 0, end: break-point)); pp.ppml-newline(); pp.space := pp.margin; blanks(pp, pp.margin - block-space); print-ppml(ppml-string(copy-sequence(str, start: break-point)), pp, distance-to-break, btype, block-space) end; end method print-ppml; define method print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); let sz = t.the-string.size + 2; if (sz <= pp.space) pp.ppml-output-string("\""); pp.ppml-output-string(t.the-string); pp.ppml-output-string("\""); pp.space := pp.space - sz; #t else // The string is too long... let str = t.the-string; let break-point = pp.space; for (i from pp.space - 3 to 0 by -1, until: str[i] = ' ') finally if (i > 0) break-point := i + 1 end end; pp.ppml-output-string("\""); pp.ppml-output-string(copy-sequence(str, start: 0, end: break-point)); pp.ppml-output-string("\""); pp.ppml-newline(); pp.space := pp.margin; blanks(pp, pp.margin - block-space); print-ppml(ppml-quote-string(copy-sequence(str, start: break-point)), pp, distance-to-break, btype, block-space) end; end method print-ppml; define method print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); if (btype ~== #"consistent" & t.blank-space + distance-to-break <= pp.space) blanks(pp, t.blank-space); #t elseif (btype == #"fit") pp.ppml-output-string(" ..."); pp.space := max(0, pp.space - 4); #f else pp.ppml-newline(); pp.space := pp.margin; blanks(pp, pp.margin - block-space + t.offset); #t end if; end method print-ppml; define method print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); pp.terse-depth := pp.terse-depth - 1; let btype = if (t.token-size + distance-to-break <= pp.space) #"inconsistent" elseif (pp.terse-depth = 0 | btype == #"fit") #"fit" else t.break-type end; print-ppml-block(t, pp, btype, pp.space - t.offset, distance-to-break); pp.terse-depth := pp.terse-depth + 1; #t end method print-ppml; define method print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); print-ppml(t.suspension-token, pp, distance-to-break, btype, block-space) end method print-ppml; define method print-ppml (t :: , pp :: , distance-to-break :: , btype :: , block-space :: ) => (truncated? :: ); print-ppml(t.ppml-equivalent, pp, distance-to-break, btype, block-space) end method print-ppml; define method print-ppml-block (t :: , pp :: , btype :: , block-space :: , after :: ) let ts = t.constituent-tokens; unless (ts.size = 0) // Compute break distance vector let sz = ts.size; let bdv = make(, size: sz); bdv[sz - 1] := after; for (i :: from sz - 1 above 0 by -1) let t = ts[i]; bdv[i - 1] := if (instance?(t, )) 0 else t.token-size + bdv[i] end; end for; for (i from 0 below ts.size, while: print-ppml(ts[i], pp, bdv[i], btype, block-space)) end for; end end; define method print-ppml-block (t :: , pp :: , btype :: , block-space :: , after :: ) let ts = t.constituent-tokens; unless (ts.size = 0) let constituents = make(, size: ts.size + (ts.size - 1) * t.separator.size); let i = 0; for (first? = #t then #f, ct in t.constituent-tokens) unless (first?) for (st in t.separator) constituents[i] := st; i := i + 1 end; end unless; constituents[i] := ct; i := i + 1 end for; print-ppml-block(ppml-block(constituents), pp, btype, block-space, after); end end; // // PPML-PRINT // //---*** andrewa+jonathan: put this in to stop crashes in the candidates. //---*** We could do with working out why... define method ppml-print (t :: , pp :: ) => () block () print-ppml(ppml-block(vector(t)), pp, 0, #"inconsistent", pp.margin); if (pp.space < pp.margin) pp.ppml-newline() end; pp.space := pp.margin; exception (error :: ) //---*** This hack is just to get around a PPML crash for now! block () format-out("PPML crashed: %s\n", error) exception (error :: ) format-out("PPML crashed, and so did printing the condition!\n") end end; values() end; define method ppml-print-one-line (t :: , pp :: ) => () ppml-print(ppml-block(vector(t), type: #"fit"), pp); values() end; // // PRINT-OBJECT methods // // These methods can be used to output a ppml token as one long string when // this is necessary. define method print-object (t :: , s :: ) => (); write(s, t.the-string); end method print-object; define method print-object (t :: , s :: ) => (); write(s, "\""); write(s, t.the-string); write(s, "\""); end method print-object; define method print-object (t :: , s :: ) => (); if (t == $line-break) write(s, "\n"); else for (i from 0 below t.blank-space) write(s, " ") end end if; values () end method print-object; define method print-object (t :: , s :: ) => (); for (ct in t.constituent-tokens) print(ct, s) end; values() end method print-object; define method print-object (t :: , s :: ) => (); for (first? = #t then #f, ct in t.constituent-tokens) unless (first?) for (st in t.separator) print(st, s) end; end; print(ct, s) end; values() end method print-object; define method print-object (t :: , s :: ) => (); print(t.suspension-token, s) end method print-object; define method print-object (t :: , s :: ) => (); print(t.the-object, s) end method print-object;