WheretIB / nullc

Fast C-like programming language with advanced features
MIT License
163 stars 13 forks source link

Precision lost with double numbers #33

Open mingodad opened 2 years ago

mingodad commented 2 years ago

When testing nullc with the tests from https://github.com/ArashPartow/exprtk there is a big difference in the total of equal expressions compared to C/C++ or other programming languages:

import std.io;
import std.math;

int total_count = 0;
int total_eq = 0;

void equal(double a, double b) {
    ++total_count;
    if(a == b) {
        ++total_eq;
    }
    //else { io.out << a << "<>" << b << io.endl;}
    else { io.out << total_count << "\t";Print(a, 16); io.out << "<>"; Print(b, 16); io.out << io.endl;}
}

double x, y, z, w;

x=11.12345678910737373;
y=22.12345678910737373;
z=33.12345678910737373;
w=44.12345678910737373;
//...
equal((-10.123456789107372000000000000000),(3-(2+abs(x))));
equal((-10.123456789107372000000000000000),(3-(abs(x)+2)));
io.out << "-10.123456789107372" << " == " << -10.123456789107372000000000000000 << " == "; Print(-10.123456789107372000000000000000, 16); io.out << io.endl;

io.out << total_count << "\t" << total_eq << io.endl;

return 0;

Output before :

-10.123456789107372 == -10.123456790947 == -10.1234567909470208
7366    417

Output after:

-10.123456789107372 == -10.123456789107 == -10.1234567891073723
7366    6923

Patch to get a better double precision and also accept numbers like .23, -.34, ...:

--------------------------- NULLC/ExpressionTree.cpp ---------------------------
index 40da9356..f6fd3dbc 100644
@@ -269,7 +269,7 @@ namespace

        if(*str == '.')
        {
-           double power = 0.1f;
+           double power = 0.1;
            str++;

            while((digit = *str - '0') < 10)
@@ -280,7 +280,7 @@ namespace
            }
        }

-       if(*str == 'e')
+       if(*str == 'e' || *str == 'E')
        {
            str++;

@@ -3792,7 +3792,7 @@ ExprBase* AnalyzeNumber(ExpressionContext &ctx, SynNumber *syntax)

    for(unsigned i = 0; i < value.length(); i++)
    {
-       if(value.begin[i] == '.' || value.begin[i] == 'e')
+       if(value.begin[i] == '.' || value.begin[i] == 'e' || value.begin[i] == 'E')
            isFP = true;
    }

------------------------------- NULLC/Lexer.cpp -------------------------------
index 6ce9c8de..4fcd7649 100644
@@ -132,7 +132,8 @@ void Lexer::Lexify(const char* code)
            }
            break;
        case '.':
-           lType = lex_point;
+                        if(isDigit(code[1])) goto do_lex_number;
+           else lType = lex_point;
            break;
        case ',':
            lType = lex_comma;
@@ -392,6 +393,7 @@ void Lexer::Lexify(const char* code)
        default:
            if(isDigit(*code))
            {
+do_lex_number:                    
                lType = lex_number;

                const char *pos = code;
@@ -408,7 +410,7 @@ void Lexer::Lexify(const char* code)
                    pos++;
                while(isDigit(*pos))
                    pos++;
-               if(*pos == 'e')
+               if(*pos == 'e' || *pos == 'E')
                {
                    pos++;
                    if(*pos == '-')

Full test source exprtk_functional_test.nc.zip

mingodad commented 2 years ago

Probably need to review other places where a double is initialized with a float.

mingodad commented 2 years ago

Also for the translated C++ to give the same result the NULLC/ExpressionTranslate.cpp need to be changed:

@@ -510,13 +510,13 @@ void TranslateIntegerLiteral(ExpressionTranslateContext &ctx, ExprIntegerLiteral
 }

 void TranslateRationalLiteral(ExpressionTranslateContext &ctx, ExprRationalLiteral *expression)
 {
    if(expression->type == ctx.ctx.typeFloat)
-       Print(ctx, "((float)%e)", expression->value);
+       Print(ctx, "((float)%.9g)", expression->value);
    else if(expression->type == ctx.ctx.typeDouble)
-       Print(ctx, "%e", expression->value);
+       Print(ctx, "%.17g", expression->value);
    else
        assert(!"unknown type");
 }

 void TranslateTypeLiteral(ExpressionTranslateContext &ctx, ExprTypeLiteral *expression)
mingodad commented 2 years ago

Hello WheretIB are you still around ? Is this project dead ?

WheretIB commented 2 years ago

I'm still around.

I will check your suggestions this weekend. Precision fixes sound great. But I'm not sure about syntax changes like supporting .1 yet, I will have to compare to other languages.

mingodad commented 2 years ago

Thank you for reply ! I'm trying to add goto and struct to nullc and changing a bit the syntax to be more close to C/C++ where possible, like I've added lex_dblcolon :: and in this branch https://github.com/mingodad/nullc/tree/dad I have the declaration of class members outside of a class using it instead of lex_colon and also paln to do the same for namespaces.

There is a reason to not using the same syntax for simple/similar constructs ?

mingodad commented 2 years ago

Would be nice if nullc could accept the subset of C++ that is used to build itself.

mingodad commented 2 years ago

Playing a bit with this project I've add a few nore C++ keywords, most of then are parsed and silently ignored for now, and I can execute this dummy script:

import std.io;

enum chartype
{
    ct_symbol = 64,         // Any symbol > 127, a-z, A-Z, 0-9, _
    ct_start_symbol = 128   // Any symbol > 127, a-z, A-Z, _, :
};

inline int give5() {return 5;}

class Klass {
    int _k;
    static int st = 0;
public:
    void Klass(int x) {
        this._k = x;
    }

    //static
    //void st_add(int x) { Klass::st += x;}
    //static void st_add(int x) { this._k += x;}
    //static
    //int st_get() { return Klass::st;}
protected:
    int get() {return this._k;}
private:
    int set(int x) {int prev = this._k; this._k = x; return prev;}
    //void finalize() {}

};

//static int Klass::st = 0;
//static
//void Klass::st_add(int x) { Klass::st += x;}

struct dad_t {
    int x, y;
};

dad_t dt;
dt.x = 3;
dt.y = 4;

Klass klass = Klass(9);

io.out << dt.x << ":" << dt.y << ":" << int(chartype.ct_symbol) << ":" << klass._k << io.endl;

return 0;

I still need to change the parser to accept access enum elements like in C++ -> chartype::ct_symbol, accept C++ style constructor/destructor(finalize), accept C++ static class members, ...

mingodad commented 2 years ago

With this experiment I want to see how far I can get to allow nullc parse the C++ subset used to build itself. Probably I would need some help in some places.

WheretIB commented 2 years ago

I have applied your fix to the precision of double number parsing, thank you for that.

I'm going to leave this issue open to address the changes to number parsing and to look into the remaining precision issues (maybe I'll just switch to strtod for a full match).

Your fork looks interesting, those are some big changes. You might call it nullc++ :)

mingodad commented 2 years ago

Also I found something strange when testing with exprtk tests, when having the the tests in the global scope it takes almost twice to process than when they are inside a function see attached exprtk_functional_test2.nc.zip .

Do you want to join effort on it (nullc++) ? I've got template on class/struct somehow working now:

import std.io;

enum chartype
{
    ct_symbol = 64,         // Any symbol > 127, a-z, A-Z, 0-9, _
    ct_start_symbol = 128   // Any symbol > 127, a-z, A-Z, _, :
};

inline int give5() {return 5;}

/*
template<typename T>
T createSome() {
    T x = new T();
    return x;
}
*/

template<typename T, classname Z>
class TName {
    T name;
    Z param;
};

typedef TName<char[], int> TNameCI;

TNameCI createSome() {
    return TNameCI();
}

TNameCI nci = createSome();
nci.name = "dad";
nci.param = 5;

struct A extendable {
    int aa;
    void A() {this.aa = 0;}
    int inc() {return ++this.aa;}
}

class Klass : public A {
    int _k;
    static int st = 0;
public:
    void Klass(int x) {
        this._k = x;
    }

    int inc() override {this.aa += 2; return this.aa;}
    //static
    //void st_add(int x) { Klass::st += x;}
    //static void st_add(int x) { this._k += x;}
    //static
    //int st_get() { return Klass::st;}
protected:
    int get() {return this._k;}
private:
    int set(int x) {int prev = this._k; this._k = x; return prev;}
    //void finalize() {}

};

//static int Klass::st = 0;
//static
//void Klass::st_add(int x) { Klass::st += x;}

struct dad_t {
    int x, y;
};

dad_t dt;
dt.x = 3;
dt.y = 4;

Klass klass = Klass(9);

io.out << dt.x << ":" << dt.y << ":" << int(chartype.ct_symbol) << ":" << klass._k << io.endl;

return 0;
mingodad commented 2 years ago

Also when trying to accepts constructor/destructor C++ syntax I noticed the restriction on finalizer (destructor) for stack instances, is this a hard restriction or can it be relaxed ?