view qt/qtd/ctfe/Format.d @ 288:f9559a957be9 signals

new signals and slots implementation
author eldar
date Sun, 08 Nov 2009 19:28:01 +0000
parents
children
line wrap: on
line source

/**
 * Compile-Time String Formatting.
 * 
 * Authors: Daniel Keep <daniel.keep@gmail.com>
 * Copyright: See LICENSE.
 */
module qt.qtd.ctfe.Format;

//debug = gb_Format_verbose;

import Integer = qt.qtd.ctfe.Integer;
import String = qt.qtd.ctfe.String;
import Tuple = qt.qtd.util.Tuple;

private
{
    string stringify(Args...)(size_t index, int alignment,
                              string opt, Args args)
    {
        if( index >= args.length )
            return "{invalid index " ~ Integer.format_ctfe(index) ~ "}";

        if( alignment != 0 )
            return "{non-zero alignments not supported yet}";

        foreach( i,_ ; Args )
        {
            if( i == index )
            {
                static if( is( Args[i] == char ) )
                {
                    string r;
                    r ~= args[i];
                    return r;
                }
                else static if( is( Args[i] : long ) || is( Args[i] : ulong ) )
                {
                    int base = 10;
                    string prefix = "";

                    if( opt == "x" )
                        base = 16;

                    else if( opt == "xx" )
                    {
                        base = 16;
                        prefix = "0x";
                    }
                    else if( opt == "o" )
                        base = 8;

                    else if( opt == "b" )
                        base = 2;

                    return prefix ~ Integer.format_ctfe(args[i], base);
                }
                else static if( is( Args[i] : string ) )
                {
                    if( opt == "x" )
                    {
                        return String.hexify_ctfe(args[i][]);
                    }
                    
                    if( opt == "q" )
                    {
                        return String.escape_ctfe(args[i][]);
                    }

                    if( opt == "l" )
                    {
                        return Integer.format_ctfe(args[i].length);
                    }
                    
                    // If you don't slice, then the CALLER has to slice the
                    // string, otherwise CTFE barfs.
                    return args[i][];
                }
                else static if( is( Args[i] Elem : Elem[] ) )
                {
                    if( opt == "l" )
                    {
                        return Integer.format_ctfe(args[i].length);
                    }
                    
                    string r = "[";
                    foreach( ei, e ; args[i][] )
                    {
                        if( ei != 0 )
                            r ~= ", ";
                        r ~= stringify(0, alignment, opt, e);
                    }
                    r ~= "]";
                    return r;
                }
                else
                {
                    return "{cannot stringify "~Args[i].stringof~"}";
                }
            }
        }

        assert(false);
    }

    version( Unittest )
    {
        static assert( stringify(0, 0, "", 0) == "0" );
        static assert( stringify(0, 0, "", 1, -2, "abc") == "1" );
        static assert( stringify(1, 0, "", 1, -2, "abc") == "-2" );
        static assert( stringify(2, 0, "", 1, -2, "abc") == "abc" );

        static assert( stringify(0, 0, "x", "abc") == `616263` );
        static assert( stringify(0, 0, "q", "abc") == `"abc"` );
        static assert( stringify(0, 0, "l", "abc") == `3` );

        static assert( stringify(0, 0, "x", 0x4a) == "4a" );

        static assert( stringify(0, 0, "", [1,2,3]) == "[1, 2, 3]" );
        static assert( stringify(0, 0, "l", [1,2,3]) == "3" );
        static assert( stringify(0, 0, "x", [9,10]) == "[9, a]" );
        static assert( stringify(0, 0, "", ["a","b"]) == "[a, b]" );
        static assert( stringify(0, 0, "q", ["a","b"]) == "[\"a\", \"b\"]" );

        static assert( stringify(0, 0, "", 'a') == "a" );
    }
}

/**
 * Substitutes a set of arguments into a template string.
 *
 * The template string allows for the following escape forms:
 *
 * - $$ -- Literal dollar.
 * - $* -- Next argument.
 * - $n -- nth argument; 0-9 only.
 * - ${} -- Next argument.
 * - ${:f} -- Next argument, using format options "f".
 * - ${n} -- nth argument.
 * - ${n:f} -- nth argument, using format options "f".
 *
 * formatNamed allows the use of named arguments (given as alternating
 * name,value pairs), but disallows "next" argument and indexed forms.
 *
 * Eventually, alignment and named arguments will be supported.
 *
 * Supported formatting options are:
 *
 * Integers:
 * - x -- format integer in hexadecimal.
 * - o -- format integer in octal.
 * - b -- format integer in binary.
 *
 * Strings:
 * - q -- quotes the string as a literal.
 * - x -- formats as hexadecimal data.
 * - l -- length of the string in decimal.
 *
 * Arrays:
 * - l -- length of the array in decimal.
 * - Other options are used to control element formatting.
 *
 * Params:
 *  tmpl    = template string.
 *  args    = arguments to substitute.
 * Returns:
 *  formatted string.
 */

string format_ctfe(Args...)(string tmpl, Args args)
{
    string r = "";
    int argPos = 0;
    
    while( tmpl.length > 0 )
    {
        bool inExp = false;
       
        // Look for a $
        foreach( i,c ; tmpl )
        {
            if (c == '$')
            {
                inExp = true;
                r ~= tmpl[0..i];
                tmpl = tmpl[i+1..$];
                break;
            }
        }

        // If we didn't find a $, it's because we hit the end of the template.
        if( !inExp )
        {
            r ~= tmpl;
            break;
        }
        
        // So we're in an expansion/substitution.

        debug(gb_Format_verbose) r ~= "{in exp}";

        if( tmpl.length == 0 )
        {
            r ~= "{unterminated substitution}";
            break;
        }

        // c is the next character, whilst tmpl is everything left in the
        // template string.
        char c = tmpl[0];
        tmpl = tmpl[1..$];
        
        // $$ - escaped $.
        if( c == '$' )
        {
            debug(gb_Format_verbose) r ~= "{escaped $}";
            r ~= '$';
            continue;
        }

        // $n - shortcut for ${n}.
        if( '0' <= c && c <= '9' )
        {
            debug(gb_Format_verbose) r ~= "{shorthand index}";
            r ~= stringify(c-'0', 0, "", args);
            continue;
        }

        // $* - shortcut for ${}
        if( c == '*' )
        {
            debug(gb_Format_verbose) r ~= "{shorthand next}";
            r ~= stringify(argPos++, 0, "", args);
            continue;
        }

        // This means we got a $ followed by something unexpected.
        if( c != '{' )
        {
            r ~= "{malformed substitution}";
            break;
        }
        
        if( tmpl.length == 0 )
        {
            r ~= "{unterminated substitution}";
            break;
        }
        
        debug(gb_Format_verbose)
        {
            r ~= "{parse complex at '";
            r ~= c;
            r ~= "':\"" ~ tmpl ~ "\"}";
        }

        // NOTE: We haven't updated c and tmpl yet.

        {
            // arg will contain the index of the argument the user wanted
            // substituted.
            size_t arg = size_t.max;
            // fmt will contain any additional formatting options.
            string fmt = "";

            // If we didn't get a : or }, that means we expect an index.
            if( !( tmpl[0] == ':' || tmpl[0] == '}' ) )
            {
                // So parse it.
                auto used = Integer.parse_ctfe!(size_t)(tmpl, true);
                
                if( used == 0 )
                {
                    debug(gb_Format_verbose) r ~= "{used zero of \""~tmpl~"\"}";
                    r ~= "{invalid argument index}";
                    break;
                }
                
                arg = Integer.parse_ctfe!(size_t)(tmpl);
                tmpl = tmpl[used..$];
                
                if( tmpl.length == 0 )
                {
                    r ~= "{unterminated substitution}";
                    break;
                }
            }
            else
            {
                // Otherwise, the index was elided, which means we want to use
                // the index of the "next" argument.
                arg = argPos;
                ++ argPos;
            }

            c = tmpl[0];
            tmpl = tmpl[1..$];

            debug(gb_Format_verbose)
                r ~= "{index " ~ Integer.format_ctfe(arg) ~ "}";

            // If c is :, then we've got formatting options to parse

            if( c == ':' )
            {
                debug(gb_Format_verbose) r ~= "{fmt string}";

                // Look for the closing }.
                size_t len = 0;
                foreach( i,d ; tmpl )
                {
                    if( d == '}' )
                    {
                        len = i;
                        break;
                    }
                }
                if( len == 0 )
                {
                    r ~= "{malformed format}";
                    break;
                }
                fmt = tmpl[0..len];
                tmpl = tmpl[len..$];

                if( tmpl.length == 0 )
                {
                    r ~= "{unterminated substitution}";
                    break;
                }

                c = tmpl[0];
                tmpl = tmpl[1..$];
            }

            // At this point, we should have the closing }.  If not, someone's
            // screwed up.
            if( c != '}' )
            {
                debug(gb_Format_verbose)
                {
                    r ~= "{expected closing; got '";
                    r ~= c;
                    r ~= "':\"" ~ tmpl ~ "\"}";
                }
                r ~= "{malformed substitution}";
                break;
            }

            // Stringify that bugger.
            r ~= stringify(arg, 0, fmt, args);

            // When we fall off the end here, we'll continue with the
            // remainder of tmpl, unless it's empty in which case we're
            // finished.
        }
    }
    
    return r;
}

version( Unittest )
{
    static assert(format_ctfe("A: $$", "foo"[]) == "A: $");
    static assert(format_ctfe("B: a $$ c", "b"[]) == "B: a $ c");
    
    static assert(format_ctfe("C: ${}", "foo"[]) == "C: foo");
    static assert(format_ctfe("D: a ${} c", "b"[]) == "D: a b c");
    
    static assert(format_ctfe("E: $0", "foo"[]) == "E: foo");
    static assert(format_ctfe("F: a $0 c", "b"[]) == "F: a b c");
    
    static assert(format_ctfe("G: $*", "foo"[]) == "G: foo");
    static assert(format_ctfe("H: a $* c", "b"[]) == "H: a b c");
    
    static assert(format_ctfe("I: ${0}", "foo"[]) == "I: foo");
    static assert(format_ctfe("J: a ${0} c", "b"[]) == "J: a b c");

    static assert(format_ctfe("K: ${} ${} ${}", 1, -2, "c"[]) == "K: 1 -2 c");
    static assert(format_ctfe("L: $* $* $*", 1, -2, "c"[]) == "L: 1 -2 c");
    static assert(format_ctfe("M: $0 $1 $2", 1, -2, "c"[]) == "M: 1 -2 c");
    static assert(format_ctfe("N: ${0} ${1} ${2}", 1, -2, "c"[]) == "N: 1 -2 c");

    static assert(format_ctfe("O: ${2} ${0} ${1}", 1, -2, "c"[]) == "O: c 1 -2");

    static assert(format_ctfe("P: ${:x} ${0:x} ${0:o} ${0:b}", 42) == "P: 2a 2a 52 101010");

    static assert(format_ctfe("Q: ${0} ${0:q} ${0:x}", "abc"[]) == "Q: abc \"abc\" 616263");
    static assert(format_ctfe("R: ${0} ${0:q}", ["a","b","c"][]) == "R: [a, b, c] [\"a\", \"b\", \"c\"]");

    const TORTURE_TMPL = `
        struct $*Enum
        {
            const Name = ${0:q};
            const string[${:l}] Members = ${1:q};

            ${2} value()
            {
                return ${3:xx};
            }
        }
    `[];

    const TORTURE_EXPECTED = `
        struct FooEnum
        {
            const Name = "Foo";
            const string[3] Members = ["bar", "quxx", "zyzzy"];

            int value()
            {
                return 0x42;
            }
        }
    `[];

    const TORTURE_ACTUAL = format_ctfe(TORTURE_TMPL,
            "Foo"[], ["bar"[],"quxx","zyzzy"][],
            "int"[], 0x42);

    static assert( TORTURE_EXPECTED == TORTURE_ACTUAL );
}

private
{
    size_t findIndexByName(Args...)(string name, Args args)
    {
        foreach( i ; Tuple.Sequence!(0, Args.length, 2) )
        {
            static if( !is( Args[i] : string ) )
            {
                static assert(false, "expected string for argument "
                        ~ Integer.format_ctfe(i) ~ " in " ~ Args.stringof
                        ~ " not " ~ Args[i].stringof);
            }
            if( name == args[i][] )
                return i+1;
        }
        return size_t.max;
    }

    version( Unittest )
    {
        static assert( findIndexByName("a", "a", 0, "b", 1) == 1 );
        static assert( findIndexByName("b", "a", 0, "b", 1) == 3 );
        static assert( findIndexByName("c", "a", 0, "b", 1) == size_t.max );
    }
}

/// ditto

string formatNamed_ctfe(Args...)(string tmpl, Args args)
{
    string r = "";
    int argPos = 0;
    
    while( tmpl.length > 0 )
    {
        bool inExp = false;
       
        // Look for a $
        foreach( i,c ; tmpl )
        {
            if (c == '$')
            {
                inExp = true;
                r ~= tmpl[0..i];
                tmpl = tmpl[i+1..$];
                break;
            }
        }

        // If we didn't find a $, it's because we hit the end of the template.
        if( !inExp )
        {
            r ~= tmpl;
            break;
        }
        
        // So we're in an expansion/substitution.

        debug(gb_Format_verbose) r ~= "{in exp}";

        if( tmpl.length == 0 )
        {
            r ~= "{unterminated substitution}";
            break;
        }

        // c is the next character, whilst tmpl is everything left in the
        // template string.
        char c = tmpl[0];
        tmpl = tmpl[1..$];
        
        // $$ - escaped $.
        if( c == '$' )
        {
            debug(gb_Format_verbose) r ~= "{escaped $}";
            r ~= '$';
            continue;
        }

        // $a... - shortcut for $a...
        if( String.isIdentStartChar_ctfe(c) )
        {
            debug(gb_Format_verbose) r ~= "{shorthand name}";
            size_t i = 0;
            while( i < tmpl.length )
            {
                if( !String.isIdentChar_ctfe(tmpl[i]) )
                    break;
                ++ i;
            }
            string name = c ~ tmpl[0..i];
            tmpl = tmpl[i..$];
            r ~= stringify(findIndexByName(name, args), 0, "", args);
            continue;
        }

        // This means we got a $ followed by something unexpected.
        if( c != '{' )
        {
            r ~= "{malformed substitution}";
            break;
        }
        
        if( tmpl.length == 0 )
        {
            r ~= "{unterminated substitution}";
            break;
        }
        
        debug(gb_Format_verbose)
        {
            r ~= "{parse complex at '";
            r ~= c;
            r ~= "':\"" ~ tmpl ~ "\"}";
        }

        // NOTE: We haven't updated c and tmpl yet.

        {
            // arg will contain the index of the argument the user wanted
            // substituted.
            size_t arg = size_t.max;
            // fmt will contain any additional formatting options.
            string fmt = "";

            // If we didn't get a : or }, that means we expect a name.
            if( !( tmpl[0] == ':' || tmpl[0] == '}' ) )
            {
                // So parse it.
                size_t i = 0;
                while( i < tmpl.length )
                {
                    if( !String.isIdentChar_ctfe(tmpl[i]) )
                        break;
                    ++ i;
                }
                string name = tmpl[0..i];
                tmpl = tmpl[i..$];

                arg = findIndexByName(name, args);
                
                if( tmpl.length == 0 )
                {
                    r ~= "{unterminated substitution}";
                    break;
                }
            }
            else
            {
                // Otherwise, the name was elided.  Kaboom!
                r ~= "{substitution missing name}";
                break;
            }

            c = tmpl[0];
            tmpl = tmpl[1..$];

            debug(gb_Format_verbose)
                r ~= "{index " ~ Integer.format_ctfe(arg) ~ "}";

            // If c is :, then we've got formatting options to parse

            if( c == ':' )
            {
                debug(gb_Format_verbose) r ~= "{fmt string}";

                // Look for the closing }.
                size_t len = 0;
                foreach( i,d ; tmpl )
                {
                    if( d == '}' )
                    {
                        len = i;
                        break;
                    }
                }
                if( len == 0 )
                {
                    r ~= "{malformed format}";
                    break;
                }
                fmt = tmpl[0..len];
                tmpl = tmpl[len..$];

                debug(gb_Format_verbose) r ~= "{fmt:"~fmt~"}";

                if( tmpl.length == 0 )
                {
                    r ~= "{unterminated substitution}";
                    break;
                }

                c = tmpl[0];
                tmpl = tmpl[1..$];
            }

            // At this point, we should have the closing }.  If not, someone's
            // screwed up.
            if( c != '}' )
            {
                debug(gb_Format_verbose)
                {
                    r ~= "{expected closing; got '";
                    r ~= c;
                    r ~= "':\"" ~ tmpl ~ "\"}";
                }
                r ~= "{malformed substitution}";
                break;
            }

            // Stringify that bugger.
            r ~= stringify(arg, 0, fmt, args);

            // When we fall off the end here, we'll continue with the
            // remainder of tmpl, unless it's empty in which case we're
            // finished.
        }
    }
    
    return r;
}

version( Unittest )
{
    static assert( formatNamed_ctfe("A: $$", "a"[], 0, "b"[], 1) == "A: $" );
    static assert( formatNamed_ctfe("B: $a", "a"[], 0, "b"[], 1) == "B: 0" );
    static assert( formatNamed_ctfe("C: $b", "a"[], 0, "b"[], 1) == "C: 1" );

    static assert( formatNamed_ctfe("D: ${a}", "a"[], 0, "b"[], 1) == "D: 0" );
    static assert( formatNamed_ctfe("E: ${b}", "a"[], 0, "b"[], 1) == "E: 1" );
    
    static assert( formatNamed_ctfe("F: $foo$bar", "foo"[], 0, "bar"[], 1) == "F: 01" );
    static assert( formatNamed_ctfe("G: ${foo}${bar}", "foo"[], 0, "bar"[], 1) == "G: 01" );

    static assert( formatNamed_ctfe("H: ${foo:x}${bar:xx}", "foo"[], 0, "bar"[], 1) == "H: 00x1" );

    const TORTURE_NAMED_TMPL = `
        struct ${name}Enum
        {
            const Name = ${name:q};
            const string[${members:l}] Members = ${members:q};

            ${retType} value()
            {
                return ${value:xx};
            }
        }
    `[];

    const TORTURE_NAMED_EXPECTED = `
        struct FooEnum
        {
            const Name = "Foo";
            const string[3] Members = ["bar", "quxx", "zyzzy"];

            int value()
            {
                return 0x42;
            }
        }
    `[];

    const TORTURE_NAMED_ACTUAL = formatNamed_ctfe(TORTURE_NAMED_TMPL,
            "name"[], "Foo"[],
            "members"[], ["bar"[],"quxx","zyzzy"][],
            "retType"[], "int"[],
            "value"[], 0x42);

    static assert( TORTURE_NAMED_EXPECTED == TORTURE_NAMED_ACTUAL );
}