/* $Id$ Part of SWI-Prolog Author: Jan Wielemaker E-mail: wielemak@science.uva.nl WWW: http://www.swi-prolog.org Copyright (C): 1985-2007, University of Amsterdam This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Formatted output (Prolog predicates format/[1,2,3]). One day, the C source should also use format() to produce error messages, etc. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "pl-incl.h" #include "pl-ctype.h" #include "pl-utf8.h" #include static char * formatNumber(bool split, int div, int radix, bool smll, Number n, Buffer out); static char * formatFloat(int how, int arg, Number f, Buffer out); #define MAXRUBBER 100 struct rubber { size_t where; /* where is rubber in output */ size_t size; /* how big should it be */ pl_wchar_t pad; /* padding character */ }; typedef struct { IOSTREAM *out; /* our output stream */ int column; /* current column */ tmp_buffer buffer; /* bin for characters with tabs */ size_t buffered; /* characters in buffer */ int pending_rubber; /* number of not-filled ~t's */ struct rubber rub[MAXRUBBER]; } format_state; #define BUFSIZE 1024 #define DEFAULT (-1) #define SHIFT { argc--; argv++; } #define NEED_ARG { if ( argc <= 0 ) \ { FMT_ERROR("not enough arguments"); \ } \ } #define FMT_ERROR(fmt) return (void)Sunlock(fd), \ PL_error(NULL, 0, NULL, ERR_FORMAT, fmt) #define FMT_ARG(c, a) return (void)Sunlock(fd), \ PL_error(NULL, 0, NULL, \ ERR_FORMAT_ARG, c, a) static int update_column(int col, int c) { switch(c) { case '\n': return 0; case '\r': return 0; case '\t': return (col + 1) | 0x7; case '\b': return (col <= 0 ? 0 : col - 1); default: return col + 1; } } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Low-level output. If there is pending rubber the output is stored in UTF-8 format in the state's `buffer'. The `buffered' field represents the number of UTF-8 characters in the buffer. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ static int outchr(format_state *state, int chr) { if ( state->pending_rubber ) { if ( chr > 0x7f ) { char buf[8]; char *s, *e; e = utf8_put_char(buf, chr); for(s=buf; sbuffer, *s, char); } else { char c = chr; addBuffer((Buffer)&state->buffer, c, char); } state->buffered++; } else { if ( Sputcode(chr, state->out) < 0 ) return FALSE; } state->column = update_column(state->column, chr); return TRUE; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Emit ASCII 0-terminated strings resulting from sprintf() on numeric arguments. No fuzz with wide characters here. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ static int outstring(format_state *state, const char *s, size_t len) { const char *q; const char *e = &s[len]; if ( state->pending_rubber ) { addMultipleBuffer(&state->buffer, s, len, char); state->buffered += len; } else { for(q=s; q < e; q++) { if ( Sputcode(*q&0xff, state->out) < 0 ) return FALSE; } } for(q=s; q < e; q++) state->column = update_column(state->column, *q&0xff); return TRUE; } static int outstring0(format_state *state, const char *s) { return outstring(state, s, strlen(s)); } static int oututf8(format_state *state, const char *s, size_t len) { const char *e = &s[len]; while(sencoding) { case ENC_ISO_LATIN_1: return outstring(state, txt->text.t, txt->length); case ENC_WCHAR: { const pl_wchar_t *s = txt->text.w; const pl_wchar_t *e = &s[txt->length]; while(sformat.predicates) static int update_column(int, Char); static bool do_format(IOSTREAM *fd, PL_chars_t *fmt, int ac, term_t av); static void distribute_rubber(struct rubber *, int, int); static int emit_rubber(format_state *state); /******************************** * PROLOG CONNECTION * ********************************/ word pl_format_predicate(term_t chr, term_t descr) { int c; predicate_t proc = NULL; Symbol s; int arity; if ( !PL_get_char_ex(chr, &c, FALSE) ) fail; if ( !get_procedure(descr, &proc, 0, GP_CREATE) ) fail; PL_predicate_info(proc, NULL, &arity, NULL); if ( arity == 0 ) return PL_error(NULL, 0, "arity must be > 0", ERR_DOMAIN, PL_new_atom("format_predicate"), descr); if ( !format_predicates ) format_predicates = newHTable(8); if ( (s = lookupHTable(format_predicates, (void *)(intptr_t)c)) ) s->value = proc; else addHTable(format_predicates, (void *)(intptr_t)c, proc); succeed; } word pl_current_format_predicate(term_t chr, term_t descr, control_t h) { GET_LD Symbol s = NULL; TableEnum e; fid_t fid; switch( ForeignControl(h) ) { case FRG_FIRST_CALL: if ( !format_predicates ) fail; e = newTableEnum(format_predicates); break; case FRG_REDO: e = ForeignContextPtr(h); break; case FRG_CUTTED: e = ForeignContextPtr(h); freeTableEnum(e); default: succeed; } if ( !(fid = PL_open_foreign_frame()) ) { freeTableEnum(e); return FALSE; } while( (s=advanceTableEnum(e)) ) { if ( PL_unify_integer(chr, (intptr_t)s->name) && PL_unify_predicate(descr, (predicate_t)s->value, 0) ) { PL_close_foreign_frame(fid); ForeignRedoPtr(e); } PL_rewind_foreign_frame(fid); } PL_close_foreign_frame(fid); freeTableEnum(e); fail; } static word format_impl(IOSTREAM *out, term_t format, term_t Args) { GET_LD term_t argv; int argc = 0; term_t args = PL_copy_term_ref(Args); int rval; PL_chars_t fmt; if ( !PL_get_text(format, &fmt, CVT_ALL|BUF_RING) ) return PL_error("format", 3, NULL, ERR_TYPE, ATOM_text, format); if ( (argc = (int)lengthList(args, FALSE)) >= 0 ) { term_t head = PL_new_term_ref(); int n = 0; argv = PL_new_term_refs(argc); while( PL_get_list(args, head, args) ) PL_put_term(argv+n++, head); } else { argc = 1; argv = PL_new_term_refs(argc); PL_put_term(argv, args); } startCritical; switch(fmt.storage) /* format can do call-back! */ { case PL_CHARS_RING: case PL_CHARS_STACK: PL_save_text(&fmt, BUF_MALLOC); break; default: break; } rval = do_format(out, &fmt, argc, argv); PL_free_text(&fmt); if ( !endCritical ) return FALSE; return rval; } word pl_format3(term_t out, term_t format, term_t args) { redir_context ctx; word rc; #if __YAP_PROLOG__ /* YAP allows the last argument to format to be of the form module:[] */ YAP_Term mod; #endif if ( (rc=setupOutputRedirect(out, &ctx, FALSE)) ) { #if __YAP_PROLOG__ /* module processing */ { args = Yap_fetch_module_for_format(args, &mod); } #endif { if ( (rc = format_impl(ctx.stream, format, args)) ) rc = closeOutputRedirect(&ctx); else discardOutputRedirect(&ctx); } #if __YAP_PROLOG__ YAP_SetCurrentModule(mod); #endif } return rc; } word pl_format(term_t fmt, term_t args) { return pl_format3(0, fmt, args); } static inline int get_chr_from_text(const PL_chars_t *t, int index) { switch(t->encoding) { case ENC_ISO_LATIN_1: return t->text.t[index]&0xff; case ENC_WCHAR: return t->text.w[index]; default: assert(0); return 0; /* not reached */ } } /******************************** * ACTUAL FORMATTING * ********************************/ static bool do_format(IOSTREAM *fd, PL_chars_t *fmt, int argc, term_t argv) { GET_LD format_state state; /* complete state */ int tab_stop = 0; /* padded tab stop */ Symbol s; unsigned int here = 0; int rc = TRUE; Slock(fd); /* buffer locally */ state.out = fd; state.pending_rubber = 0; initBuffer(&state.buffer); state.buffered = 0; if ( fd->position ) state.column = fd->position->linepos; else state.column = 0; while(here < fmt->length) { int c = get_chr_from_text(fmt, here); switch(c) { case '~': { int arg = DEFAULT; /* Numeric argument */ /* Get the numeric argument */ c = get_chr_from_text(fmt, ++here); if ( isDigitW(c) ) { arg = c - '0'; here++; while(here < fmt->length) { c = get_chr_from_text(fmt, here); if ( isDigitW(c) ) { int dw = c - '0'; int arg2 = arg*10 + dw; if ( (arg2 - dw)/10 != arg ) /* see mul64() in pl-arith.c */ { FMT_ERROR("argument overflow"); } arg = arg2; here++; } else break; } } else if ( c == '*' ) { NEED_ARG; if ( PL_get_integer(argv, &arg) ) { SHIFT; } else FMT_ERROR("no or negative integer for `*' argument"); c = get_chr_from_text(fmt, ++here); } else if ( c == '`' && here < fmt->length ) { arg = get_chr_from_text(fmt, ++here); c = get_chr_from_text(fmt, ++here); } /* Check for user defined format */ if ( format_predicates && (s = lookupHTable(format_predicates, (void*)((intptr_t)c))) ) { predicate_t proc = (predicate_t) s->value; int arity; term_t av; char buf[BUFSIZE]; char *str = buf; size_t bufsize = BUFSIZE; unsigned int i; PL_predicate_info(proc, NULL, &arity, NULL); av = PL_new_term_refs(arity); if ( arg == DEFAULT ) PL_put_atom(av+0, ATOM_default); else PL_put_integer(av+0, arg); for(i=1; i < arity; i++) { NEED_ARG; PL_put_term(av+i, argv); SHIFT; } tellString(&str, &bufsize, ENC_UTF8); rc = PL_call_predicate(NULL, PL_Q_PASS_EXCEPTION, proc, av); toldString(); if ( !rc ) { if ( str != buf ) free(str); goto out; } oututf8(&state, str, bufsize); if ( str != buf ) free(str); here++; } else { switch(c) /* Build in formatting */ { case 'a': /* atomic */ { PL_chars_t txt; NEED_ARG; if ( !PL_get_text(argv, &txt, CVT_ATOMIC) ) FMT_ARG("a", argv); SHIFT; outtext(&state, &txt); here++; break; } case 'c': /* ~c: character code */ { int chr; NEED_ARG; if ( PL_get_integer(argv, &chr) && chr >= 0 ) { int times = (arg == DEFAULT ? 1 : arg); SHIFT; while(times-- > 0) { outchr(&state, chr); } } else FMT_ARG("c", argv); here++; break; } case 'e': /* exponential float */ case 'E': /* Exponential float */ case 'f': /* float */ case 'g': /* shortest of 'f' and 'e' */ case 'G': /* shortest of 'f' and 'E' */ { number n; union { tmp_buffer b; buffer b1; } u; NEED_ARG; if ( !valueExpression(argv, &n PASS_LD) ) { char f[2]; f[0] = c; f[1] = EOS; FMT_ARG(f, argv); } SHIFT; initBuffer(&u.b); formatFloat(c, arg, &n, &u.b1); clearNumber(&n); outstring0(&state, baseBuffer(&u.b, char)); discardBuffer(&u.b); here++; break; } case 'd': /* integer */ case 'D': /* grouped integer */ case 'r': /* radix number */ case 'R': /* Radix number */ { number i; tmp_buffer b; NEED_ARG; if ( !valueExpression(argv, &i PASS_LD) || !toIntegerNumber(&i, 0) ) { char f[2]; f[0] = c; f[1] = EOS; FMT_ARG(f, argv); } SHIFT; if ( arg == DEFAULT ) arg = 0; initBuffer(&b); if ( c == 'd' || c == 'D' ) { formatNumber(c == 'D', arg, 10, TRUE, &i, (Buffer)&b); } else { if ( arg < 1 || arg > 36 ) { term_t r = PL_new_term_ref(); PL_put_integer(r, arg); Sunlock(fd); return PL_error(NULL, 0, NULL, ERR_DOMAIN, ATOM_radix, r); } formatNumber(FALSE, 0, arg, c == 'r', &i, (Buffer)&b); } clearNumber(&i); outstring0(&state, baseBuffer(&b, char)); discardBuffer(&b); here++; break; } case 's': /* string */ { PL_chars_t txt; NEED_ARG; if ( !PL_get_text(argv, &txt, CVT_LIST|CVT_STRING) && !PL_get_text(argv, &txt, CVT_ATOM) ) /* SICStus compat */ FMT_ARG("s", argv); outtext(&state, &txt); SHIFT; here++; break; } case 'i': /* ignore */ { NEED_ARG; SHIFT; here++; break; } { Func f; char buf[BUFSIZE]; char *str; case 'k': /* write_canonical */ f = pl_write_canonical; goto pl_common; case 'p': /* print */ f = pl_print; goto pl_common; case 'q': /* writeq */ f = pl_writeq; goto pl_common; case 'w': /* write */ f = pl_write; pl_common: NEED_ARG; if ( state.pending_rubber ) { size_t bufsize = BUFSIZE; str = buf; tellString(&str, &bufsize, ENC_UTF8); (*f)(argv); toldString(); oututf8(&state, str, bufsize); if ( str != buf ) free(str); } else { if ( fd->position && fd->position->linepos == state.column ) { IOSTREAM *old = Scurout; Scurout = fd; rc = (int)(*f)(argv); Scurout = old; if ( !rc ) goto out; state.column = fd->position->linepos; } else { size_t bufsize = BUFSIZE; str = buf; tellString(&str, &bufsize, ENC_UTF8); (*f)(argv); toldString(); oututf8(&state, str, bufsize); if ( str != buf ) free(str); } } SHIFT; here++; break; } case 'W': /* write_term(Value, Options) */ { char buf[BUFSIZE]; char *str; if ( argc < 2 ) { FMT_ERROR("not enough arguments"); } if ( state.pending_rubber ) { size_t bufsize = BUFSIZE; str = buf; tellString(&str, &bufsize, ENC_UTF8); rc = (int)pl_write_term(argv, argv+1); toldString(); if ( !rc ) goto out; oututf8(&state, str, bufsize); if ( str != buf ) free(str); } else { if ( fd->position && fd->position->linepos == state.column ) { IOSTREAM *old = Scurout; Scurout = fd; rc = (int)pl_write_term(argv, argv+1); Scurout = old; if ( !rc ) goto out; state.column = fd->position->linepos; } else { size_t bufsize = BUFSIZE; str = buf; tellString(&str, &bufsize, ENC_UTF8); rc = (int)pl_write_term(argv, argv+1); if ( !rc ) goto out; toldString(); oututf8(&state, str, bufsize); if ( str != buf ) free(str); } } SHIFT; SHIFT; here++; break; } case '@': { char buf[BUFSIZE]; char *str = buf; size_t bufsize = BUFSIZE; term_t ex = 0; int rval; if ( argc < 1 ) { FMT_ERROR("not enough arguments"); } tellString(&str, &bufsize, ENC_UTF8); rval = callProlog(NULL, argv, PL_Q_CATCH_EXCEPTION, &ex); toldString(); oututf8(&state, str, bufsize); if ( str != buf ) free(str); if ( !rval ) { Sunlock(fd); if ( ex ) return PL_raise_exception(ex); else fail; } SHIFT; here++; break; } case '~': /* ~ */ { outchr(&state, '~'); here++; break; } case 'n': /* \n */ case 'N': /* \n if not on newline */ { if ( arg == DEFAULT ) arg = 1; if ( c == 'N' && state.column == 0 ) arg--; while( arg-- > 0 ) outchr(&state, '\n'); here++; break; } case 't': /* insert tab */ { if ( state.pending_rubber >= MAXRUBBER ) FMT_ERROR("Too many tab stops"); state.rub[state.pending_rubber].where = state.buffered; state.rub[state.pending_rubber].pad = (arg == DEFAULT ? (pl_wchar_t)' ' : (pl_wchar_t)arg); state.rub[state.pending_rubber].size = 0; state.pending_rubber++; here++; break; } case '|': /* set tab */ { int stop; if ( arg == DEFAULT ) arg = state.column; case '+': /* tab relative */ if ( arg == DEFAULT ) arg = 8; stop = (c == '+' ? tab_stop + arg : arg); if ( state.pending_rubber == 0 ) /* nothing to distribute */ { state.rub[0].where = state.buffered; state.rub[0].pad = ' '; state.pending_rubber++; } distribute_rubber(state.rub, state.pending_rubber, stop - state.column); emit_rubber(&state); state.column = tab_stop = stop; here++; break; } default: { term_t ex = PL_new_term_ref(); Sunlock(fd); PL_put_atom(ex, codeToAtom(c)); return PL_error("format", 2, NULL, ERR_EXISTENCE, PL_new_atom("format_character"), ex); } } } break; /* the '~' switch */ } default: { outchr(&state, c); here++; break; } } } if ( state.pending_rubber ) /* not closed ~t: flush out */ emit_rubber(&state); out: Sunlock(fd); return rc; } static void distribute_rubber(struct rubber *r, int rn, int space) { if ( space > 0 ) { int s = space / rn; int n, m; for(n=0; n < rn; n++) /* give them equal size */ r[n].size = s; /* distribute from the center */ space -= s*rn; for(m = rn / 2, n = 0; space; n++, space--) { r[m + (n % 2 ? n : -n)].size++; } } else { int n; for(n=0; n < rn; n++) /* set all rubber to 0 */ r[n].size = 0; } } static int emit_rubber(format_state *state) { const char *s = baseBuffer(&state->buffer, char); const char *e = &s[entriesBuffer(&state->buffer, char)]; struct rubber *r = state->rub; int rn = state->pending_rubber; size_t j; for(j = 0; s <= e; j++) { int chr; if ( rn && r->where == j ) { size_t n; for(n=0; nsize; n++) { if ( Sputcode(r->pad, state->out) < 0 ) return FALSE; } r++; rn--; } if ( s < e ) { s = utf8_get_char(s, &chr); if ( Sputcode(chr, state->out) < 0 ) return FALSE; } else break; } discardBuffer(&state->buffer); initBuffer(&state->buffer); state->buffered = 0; state->pending_rubber = 0; return TRUE; } /* format an integer according to a number of modifiers at various radius. `split' is a boolean asking to put ',' between each group of three digits (e.g. 67,567,288). `div' askes to divide the number by radix^`div' before printing. `radix' is the radix used for conversion. `n' is the number to be converted. ** Fri Aug 19 22:26:41 1988 jan@swivax.UUCP (Jan Wielemaker) */ static char * formatNumber(bool split, int div, int radix, bool smll, Number i, Buffer out) { switch(i->type) { case V_INTEGER: { int64_t n = i->value.i; char buf[100]; char *tmp, *end, *s; int before = (div == 0); int digits = 0; bool negative = FALSE; if ( div+3 > (int)sizeof(buf) ) /* 0.000NNNN with div digits after 0. */ { tmp = PL_malloc(div+3); end = tmp+div+3; } else { tmp = buf; end = tmp+sizeof(buf); } s = end; /* i.e. start at the end */ *--s = EOS; if ( n < 0 ) { n = -n; negative = TRUE; } if ( n == 0 && div == 0 ) { *--s = '0'; } else { while( n > 0 || div >= 0 ) { if ( div-- == 0 && !before ) { *--s = '.'; before = 1; } if ( split && before && (digits++ % 3) == 0 && digits != 1 ) *--s = ','; *--s = digitName((int)(n % radix), smll); n /= radix; } if ( negative ) *--s = '-'; } addMultipleBuffer(out, s, end-s, char); if ( tmp != buf ) PL_free(tmp); return baseBuffer(out, char); } #ifdef O_GMP case V_MPZ: { size_t len = mpz_sizeinbase(i->value.mpz, radix); char tmp[256]; char *buf; if ( len+2 > sizeof(tmp) ) buf = PL_malloc(len+2); else buf = tmp; mpz_get_str(buf, radix, i->value.mpz); if ( !smll && radix > 10 ) { char *s; for(s=buf; *s; s++) *s = toupper(*s); } if ( split || div > 0 ) { int before = (int)(len-div); int leading; char *s = buf; if ( *s == '-' ) { addBuffer(out, *s, char); s++; } if ( split ) { leading = before % 3; if ( leading == 0 ) leading = 3; } else { leading = (int)len; } for(; *s; s++) { if ( before-- == 0 && div > 0 ) { addBuffer(out, '.', char); } else if ( leading-- == 0 && before > 0 ) { addBuffer(out, ',', char); leading = 2; } addBuffer(out, *s, char); } addBuffer(out, EOS, char); } else { addMultipleBuffer(out, buf, strlen(buf), char); addBuffer(out, EOS, char); } if ( buf != tmp ) PL_free(buf); return baseBuffer(out, char); } #endif /*O_GMP*/ default: assert(0); return NULL; } } static char * formatFloat(int how, int arg, Number f, Buffer out) { if ( arg == DEFAULT ) arg = 6; switch(f->type) { #ifdef O_GMP mpf_t mpf; case V_MPZ: mpf_init2(mpf, arg*4); mpf_set_z(mpf, f->value.mpz); goto print; case V_MPQ: { char tmp[12]; int size; int written; int fbits; switch(how) { case 'f': case 'g': case 'G': { mpz_t iv; mpz_init(iv); mpz_set_q(iv, f->value.mpq); fbits = (int)mpz_sizeinbase(iv, 2) + 4*arg; mpz_clear(iv); break; } default: fbits = 4*arg; } mpf_init2(mpf, fbits); mpf_set_q(mpf, f->value.mpq); print: Ssprintf(tmp, "%%.%dF%c", arg, how); size = 0; written = arg+4; while(written >= size) { size = written+1; growBuffer(out, size); /* reserve for -.e */ written = gmp_snprintf(baseBuffer(out, char), size, tmp, mpf); } mpf_clear(mpf); out->top = out->base + written; return baseBuffer(out, char); } #endif case V_INTEGER: promoteToFloatNumber(f); /*FALLTHROUGH*/ case V_FLOAT: { char tmp[12]; int written = arg+20; int size = 0; Ssprintf(tmp, "%%.%d%c", arg, how); while(written >= size) { size = written+1; growBuffer(out, size); written = snprintf(baseBuffer(out, char), size, tmp, f->value.f); } out->top = out->base + written; return baseBuffer(out, char); } } return NULL; }