blueloveTH 1 year ago
parent
commit
803e7f1791

+ 1 - 0
include/pocketpy/common/vector.h

@@ -34,6 +34,7 @@ c11_vector c11_vector__copy(const c11_vector* self);
 void c11_vector__reserve(c11_vector* self, int capacity);
 void c11_vector__clear(c11_vector* self);
 void* c11_vector__emplace(c11_vector* self);
+bool c11_vector__contains(const c11_vector* self, void* elem);
 c11_array c11_vector__submit(c11_vector* self);
 
 #define c11__getitem(T, self, index) (((T*)(self)->data)[index])

+ 30 - 17
include/pocketpy/objects/codeobject.h

@@ -7,14 +7,15 @@
 #include "pocketpy/common/smallmap.h"
 #include "pocketpy/objects/base.h"
 #include "pocketpy/objects/sourcedata.h"
+#include "pocketpy/objects/namedict.h"
 #include "pocketpy/pocketpy.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-#define BC_NOARG        0
-#define BC_KEEPLINE     -1
+#define BC_NOARG 0
+#define BC_KEEPLINE -1
 
 typedef enum FuncType {
     FuncType_UNSET,
@@ -39,9 +40,10 @@ typedef enum CodeBlockType {
 } CodeBlockType;
 
 typedef enum Opcode {
-    #define OPCODE(name) OP_##name,
-    #include "pocketpy/xmacros/opcodes.h"
-    #undef OPCODE
+
+#define OPCODE(name) OP_##name,
+#include "pocketpy/xmacros/opcodes.h"
+#undef OPCODE
 } Opcode;
 
 typedef struct Bytecode {
@@ -70,18 +72,18 @@ typedef struct CodeObject {
     pk_SourceData_ src;
     c11_string* name;
 
-    c11_vector/*T=Bytecode*/                codes;
-    c11_vector/*T=CodeObjectByteCodeEx*/    codes_ex;
+    c11_vector /*T=Bytecode*/ codes;
+    c11_vector /*T=CodeObjectByteCodeEx*/ codes_ex;
 
-    c11_vector/*T=py_TValue*/   consts;     // constants
-    c11_vector/*T=py_Name*/ varnames;   // local variables
+    c11_vector /*T=py_TValue*/ consts;  // constants
+    c11_vector /*T=py_Name*/ varnames;  // local variables
     int nlocals;                        // cached varnames.size()
 
     c11_smallmap_n2i varnames_inv;
     c11_smallmap_n2i labels;
 
-    c11_vector/*T=CodeBlock*/ blocks;
-    c11_vector/*T=FuncDecl_*/ func_decls;
+    c11_vector /*T=CodeBlock*/ blocks;
+    c11_vector /*T=FuncDecl_*/ func_decls;
 
     int start_line;
     int end_line;
@@ -91,18 +93,18 @@ void CodeObject__ctor(CodeObject* self, pk_SourceData_ src, c11_sv name);
 void CodeObject__dtor(CodeObject* self);
 void CodeObject__gc_mark(const CodeObject* self);
 
-typedef struct FuncDeclKwArg{
-    int index;    // index in co->varnames
-    uint16_t key;  // name of this argument
+typedef struct FuncDeclKwArg {
+    int index;        // index in co->varnames
+    uint16_t key;     // name of this argument
     py_TValue value;  // default value
 } FuncDeclKwArg;
 
 typedef struct FuncDecl {
     RefCounted rc;
-    CodeObject code;    // strong ref
+    CodeObject code;  // strong ref
 
-    c11_vector/*T=int*/     args;   // indices in co->varnames
-    c11_vector/*T=KwArg*/ kwargs;   // indices in co->varnames
+    c11_vector /*T=int*/ args;      // indices in co->varnames
+    c11_vector /*T=KwArg*/ kwargs;  // indices in co->varnames
 
     int starred_arg;    // index in co->varnames, -1 if no *arg
     int starred_kwarg;  // index in co->varnames, -1 if no **kwarg
@@ -121,6 +123,17 @@ void FuncDecl__dtor(FuncDecl* self);
 void FuncDecl__add_kwarg(FuncDecl* self, int index, uint16_t key, const py_TValue* value);
 void FuncDecl__gc_mark(const FuncDecl* self);
 
+// runtime function
+typedef struct Function {
+    FuncDecl_ decl;
+    PyObject* module;     // weak ref
+    PyObject* clazz;      // weak ref
+    pk_NameDict* closure;  // strong ref
+} Function;
+
+void Function__ctor(Function* self, FuncDecl_ decl, PyObject* module);
+void Function__dtor(Function* self);
+
 #ifdef __cplusplus
 }
 #endif

+ 8 - 0
src/common/vector.c

@@ -63,6 +63,14 @@ void* c11_vector__emplace(c11_vector* self){
     return p;
 }
 
+bool c11_vector__contains(const c11_vector *self, void *elem){
+    for(int i = 0; i < self->count; i++){
+        void* p = (char*)self->data + self->elem_size * i;
+        if(memcmp(p, elem, self->elem_size) == 0) return true;
+    }
+    return false;
+}
+
 c11_array c11_vector__submit(c11_vector* self){
     c11_array retval = {
         .data = self->data,

+ 169 - 2
src/compiler/compiler.c

@@ -635,7 +635,6 @@ static void _load_simple_expr(Ctx* ctx, c11_sv expr, int line) {
     // name or name.name
     bool is_fastpath = false;
     if(is_identifier(expr)) {
-        // ctx->emit_(OP_LOAD_NAME, py_Name(expr.sv()).index, line);
         Ctx__emit_(ctx, OP_LOAD_NAME, py_name2(expr), line);
         is_fastpath = true;
     } else {
@@ -2188,6 +2187,174 @@ Error* try_compile_assignment(Compiler* self, bool* is_assign) {
     return NULL;
 }
 
+static FuncDecl_ push_f_context(Compiler* self, c11_sv name, int* out_index) {
+    FuncDecl_ decl = FuncDecl__rcnew(self->src, name);
+    decl->code.start_line = self->i == 0 ? 1 : prev()->line;
+    decl->nested = name_scope(self) == NAME_LOCAL;
+    // add_func_decl
+    Ctx* top_ctx = ctx();
+    c11_vector__push(FuncDecl_, &top_ctx->co->func_decls, decl);
+    *out_index = top_ctx->co->func_decls.count - 1;
+    // push new context
+    top_ctx = c11_vector__emplace(&self->contexts);
+    // contexts.push_back(CodeEmitContext(vm, decl->code, contexts.size()));
+    Ctx__ctor(top_ctx, &decl->code, decl, self->contexts.count);
+    return decl;
+}
+
+static Error* read_literal(Compiler* self, py_Ref out) {
+    Error* err;
+    advance();
+    const TokenValue* value = &prev()->value;
+    bool negated = false;
+    switch(prev()->type) {
+        case TK_SUB:
+            consume(TK_NUM);
+            value = &prev()->value;
+            negated = true;
+        case TK_NUM: {
+            if(value->index == TokenValue_I64) {
+                py_newint(out, negated ? -value->_i64 : value->_i64);
+            } else if(value->index == TokenValue_F64) {
+                py_newfloat(out, negated ? -value->_f64 : value->_f64);
+            } else {
+                return SyntaxError();
+            }
+            return NULL;
+        }
+        case TK_STR: py_newstr(out, value->_str->data); return NULL;
+        case TK_TRUE: py_newbool(out, true); return NULL;
+        case TK_FALSE: py_newbool(out, false); return NULL;
+        case TK_NONE: py_newnone(out); return NULL;
+        case TK_DOTDOTDOT: py_newellipsis(out); return NULL;
+        case TK_LPAREN: {
+            py_TValue cpnts[4];
+            int count = 0;
+            while(true) {
+                if(count == 4) return SyntaxError("default argument tuple exceeds 4 elements");
+                check(read_literal(self, &cpnts[count]));
+                count += 1;
+                if(curr()->type == TK_RPAREN) break;
+                consume(TK_COMMA);
+                if(curr()->type == TK_RPAREN) break;
+            }
+            consume(TK_RPAREN);
+            py_newtuple(out, count);
+            for(int i = 0; i < count; i++) {
+                py_tuple__setitem(out, i, &cpnts[i]);
+            }
+            return NULL;
+        }
+        default: *out = PY_NIL; return NULL;
+    }
+}
+
+static Error* _compile_f_args(Compiler* self, FuncDecl* decl, bool enable_type_hints) {
+    int state = 0;  // 0 for args, 1 for *args, 2 for k=v, 3 for **kwargs
+    Error* err;
+    do {
+        if(state > 3) return SyntaxError();
+        if(state == 3) return SyntaxError("**kwargs should be the last argument");
+        match_newlines();
+        if(match(TK_MUL)) {
+            if(state < 1)
+                state = 1;
+            else
+                return SyntaxError("*args should be placed before **kwargs");
+        } else if(match(TK_POW)) {
+            state = 3;
+        }
+        consume(TK_ID);
+        py_Name name = py_name2(Token__sv(prev()));
+
+        // check duplicate argument name
+        py_Name tmp_name;
+        c11__foreach(int, &decl->args, j) {
+            tmp_name = c11__getitem(py_Name, &decl->args, *j);
+            if(tmp_name == name) return SyntaxError("duplicate argument name");
+        }
+        c11__foreach(FuncDeclKwArg, &decl->kwargs, kv) {
+            tmp_name = c11__getitem(py_Name, &decl->code.varnames, kv->index);
+            if(tmp_name == name) return SyntaxError("duplicate argument name");
+        }
+        if(decl->starred_arg != -1) {
+            tmp_name = c11__getitem(py_Name, &decl->code.varnames, decl->starred_arg);
+            if(tmp_name == name) return SyntaxError("duplicate argument name");
+        }
+        if(decl->starred_kwarg != -1) {
+            tmp_name = c11__getitem(py_Name, &decl->code.varnames, decl->starred_kwarg);
+            if(tmp_name == name) return SyntaxError("duplicate argument name");
+        }
+
+        // eat type hints
+        if(enable_type_hints && match(TK_COLON)) check(consume_type_hints(self));
+        if(state == 0 && curr()->type == TK_ASSIGN) state = 2;
+        int index = Ctx__add_varname(ctx(), name);
+        switch(state) {
+            case 0: c11_vector__push(int, &decl->args, index); break;
+            case 1:
+                decl->starred_arg = index;
+                state += 1;
+                break;
+            case 2: {
+                consume(TK_ASSIGN);
+                py_TValue value;
+                check(read_literal(self, &value));
+                if(py_isnil(&value)) return SyntaxError("default argument must be a literal");
+                FuncDecl__add_kwarg(decl, index, name, &value);
+            } break;
+            case 3:
+                decl->starred_kwarg = index;
+                state += 1;
+                break;
+        }
+    } while(match(TK_COMMA));
+    return NULL;
+}
+
+static Error* compile_function(Compiler* self, int decorators) {
+    Error* err;
+    consume(TK_ID);
+    c11_sv decl_name = Token__sv(prev());
+    int decl_index;
+    FuncDecl_ decl = push_f_context(self, decl_name, &decl_index);
+    consume(TK_LPAREN);
+    if(!match(TK_RPAREN)) {
+        check(_compile_f_args(self, decl, true));
+        consume(TK_RPAREN);
+    }
+    if(match(TK_ARROW)) check(consume_type_hints(self));
+    check(compile_block_body(self, compile_stmt));
+    check(pop_context(self));
+
+    if(decl->code.codes.count >= 2) {
+        Bytecode* codes = (Bytecode*)decl->code.codes.data;
+
+        if(codes[0].op == OP_LOAD_CONST && codes[1].op == OP_POP_TOP) {
+            // handle optional docstring
+            py_TValue* consts = decl->code.consts.data;
+            py_TValue* c = &consts[codes[0].arg];
+            if(py_isstr(c)) {
+                decl->docstring = py_tostr(c);
+                codes[0].op = OP_NO_OP;
+                codes[1].op = OP_NO_OP;
+            }
+        }
+    }
+
+    Ctx__emit_(ctx(), OP_LOAD_FUNCTION, decl_index, prev()->line);
+    Ctx__s_emit_decorators(ctx(), decorators);
+
+    if(ctx()->is_compiling_class) {
+        Ctx__emit_(ctx(), OP_STORE_CLASS_ATTR, py_name2(decl_name), prev()->line);
+    } else {
+        NameExpr* e = NameExpr__new(prev()->line, py_name2(decl_name), name_scope(self));
+        vtemit_store((Expr*)e, ctx());
+        vtdelete((Expr*)e);
+    }
+    return NULL;
+}
+
 static Error* compile_stmt(Compiler* self) {
     Error* err;
     if(match(TK_CLASS)) {
@@ -2244,7 +2411,7 @@ static Error* compile_stmt(Compiler* self) {
         case TK_FOR: check(compile_for_loop(self)); break;
         // case TK_IMPORT: check(compile_normal_import()); break;
         // case TK_FROM: check(compile_from_import()); break;
-        // case TK_DEF: check(compile_function()); break;
+        case TK_DEF: check(compile_function(self, 0)); break;
         // case TK_DECORATOR: check(compile_decorated()); break;
         // case TK_TRY: check(compile_try_except()); break;
         case TK_PASS: consume_end_stmt(); break;

+ 11 - 14
src/interpreter/ceval.c

@@ -164,20 +164,17 @@ pk_FrameResult pk_VM__run_top_frame(pk_VM* self) {
             /*****************************************/
             case OP_LOAD_ELLIPSIS: py_newellipsis(SP()++); DISPATCH();
             case OP_LOAD_FUNCTION: {
-                // FuncDecl_ decl = c11__getitem(FuncDecl_, &frame->co->func_decls, byte.arg);
-                // py_TValue obj;
-                // if(decl->nested) {
-                //     NameDict* captured = frame->_locals.to_namedict();
-                //     obj =
-                //         new_object<Function>(tp_function, decl, frame->_module, nullptr,
-                //         captured);
-                //     uint16_t name = py_Name__map2(py_Str__sv(&decl->code->name));
-                //     captured->set(name, obj);
-                // } else {
-                //     obj = new_object<Function>(tp_function, decl, frame->_module, nullptr,
-                //     nullptr);
-                // }
-                // PUSH(obj);DISPATCH();
+                FuncDecl_ decl = c11__getitem(FuncDecl_, &frame->co->func_decls, byte.arg);
+                Function* ud = py_newobject(SP(), tp_function, 0, sizeof(Function));
+                Function__ctor(ud, decl, frame->module);
+                if(decl->nested) {
+                    ud->closure = FastLocals__to_namedict(frame->locals, frame->locals_co);
+                    py_Name name = py_name2(c11_string__sv(decl->code.name));
+                    // capture itself to allow recursion
+                    pk_NameDict__set(ud->closure, name, *SP());
+                }
+                SP()++;
+                DISPATCH();
             }
             case OP_LOAD_NULL:
                 py_newnil(SP()++);

+ 22 - 22
src/interpreter/frame.c

@@ -6,11 +6,9 @@ void ValueStack__ctor(ValueStack* self) {
     self->end = self->begin + PK_VM_STACK_SIZE;
 }
 
-void ValueStack__clear(ValueStack* self) {
-    self->sp = self->begin;
-}
+void ValueStack__clear(ValueStack* self) { self->sp = self->begin; }
 
-py_TValue* FastLocals__try_get_by_name(py_TValue* locals, const CodeObject* co, py_Name name){
+py_TValue* FastLocals__try_get_by_name(py_TValue* locals, const CodeObject* co, py_Name name) {
     int index = c11_smallmap_n2i__get(&co->varnames_inv, name, -1);
     if(index == -1) return NULL;
     return &locals[index];
@@ -20,14 +18,12 @@ pk_NameDict* FastLocals__to_namedict(py_TValue* locals, const CodeObject* co) {
     pk_NameDict* dict = pk_NameDict__new();
     c11__foreach(c11_smallmap_n2i_KV, &co->varnames_inv, entry) {
         py_TValue value = locals[entry->value];
-        if(!py_isnil(&value)){
-            pk_NameDict__set(dict, entry->key, value);
-        }
+        if(!py_isnil(&value)) { pk_NameDict__set(dict, entry->key, value); }
     }
     return dict;
 }
 
-UnwindTarget* UnwindTarget__new(UnwindTarget* next, int iblock, int offset){
+UnwindTarget* UnwindTarget__new(UnwindTarget* next, int iblock, int offset) {
     UnwindTarget* self = malloc(sizeof(UnwindTarget));
     self->next = next;
     self->iblock = iblock;
@@ -35,11 +31,14 @@ UnwindTarget* UnwindTarget__new(UnwindTarget* next, int iblock, int offset){
     return self;
 }
 
-void UnwindTarget__delete(UnwindTarget* self){
-    free(self);
-}
+void UnwindTarget__delete(UnwindTarget* self) { free(self); }
 
-Frame* Frame__new(const CodeObject* co, const py_TValue* module, const py_TValue* function, py_TValue* p0, py_TValue* locals, const CodeObject* locals_co){
+Frame* Frame__new(const CodeObject* co,
+                  const py_TValue* module,
+                  const py_TValue* function,
+                  py_TValue* p0,
+                  py_TValue* locals,
+                  const CodeObject* locals_co) {
     static_assert(sizeof(Frame) <= kPoolFrameBlockSize, "!(sizeof(Frame) <= kPoolFrameBlockSize)");
     Frame* self = PoolFrame_alloc();
     self->f_back = NULL;
@@ -54,7 +53,7 @@ Frame* Frame__new(const CodeObject* co, const py_TValue* module, const py_TValue
     return self;
 }
 
-void Frame__delete(Frame* self){
+void Frame__delete(Frame* self) {
     while(self->uw_list) {
         UnwindTarget* p = self->uw_list;
         self->uw_list = p->next;
@@ -63,7 +62,7 @@ void Frame__delete(Frame* self){
     PoolFrame_dealloc(self);
 }
 
-int Frame__prepare_jump_exception_handler(Frame* self, ValueStack* _s){
+int Frame__prepare_jump_exception_handler(Frame* self, ValueStack* _s) {
     // try to find a parent try block
     int iblock = Frame__iblock(self);
     while(iblock >= 0) {
@@ -74,15 +73,16 @@ int Frame__prepare_jump_exception_handler(Frame* self, ValueStack* _s){
     if(iblock < 0) return -1;
     py_TValue obj = *--_s->sp;  // pop exception object
     UnwindTarget* uw = Frame__find_unwind_target(self, iblock);
-    _s->sp = (self->locals + uw->offset);  // unwind the stack                          
-    *(_s->sp++) = obj;      // push it back
+    _s->sp = (self->locals + uw->offset);  // unwind the stack
+    *(_s->sp++) = obj;                     // push it back
     return c11__at(CodeBlock, &self->co->blocks, iblock)->end;
 }
 
-void Frame__prepare_jump_break(Frame* self, ValueStack* _s, int target){
+void Frame__prepare_jump_break(Frame* self, ValueStack* _s, int target) {
     int iblock = Frame__iblock(self);
     if(target >= self->co->codes.count) {
-        while(iblock >= 0) iblock = Frame__exit_block(self, _s, iblock);
+        while(iblock >= 0)
+            iblock = Frame__exit_block(self, _s, iblock);
     } else {
         // BUG (solved)
         // for i in range(4):
@@ -96,14 +96,14 @@ void Frame__prepare_jump_break(Frame* self, ValueStack* _s, int target){
     }
 }
 
-int Frame__prepare_loop_break(Frame* self, ValueStack* _s){
+int Frame__prepare_loop_break(Frame* self, ValueStack* _s) {
     int iblock = Frame__iblock(self);
     int target = c11__getitem(CodeBlock, &self->co->blocks, iblock).end;
     Frame__prepare_jump_break(self, _s, target);
     return target;
 }
 
-int Frame__exit_block(Frame* self, ValueStack* _s, int iblock){
+int Frame__exit_block(Frame* self, ValueStack* _s, int iblock) {
     CodeBlock* block = c11__at(CodeBlock, &self->co->blocks, iblock);
     if(block->type == CodeBlockType_FOR_LOOP) {
         _s->sp--;  // pop iterator
@@ -113,7 +113,7 @@ int Frame__exit_block(Frame* self, ValueStack* _s, int iblock){
     return block->parent;
 }
 
-UnwindTarget* Frame__find_unwind_target(Frame* self, int iblock){
+UnwindTarget* Frame__find_unwind_target(Frame* self, int iblock) {
     UnwindTarget* uw;
     for(uw = self->uw_list; uw; uw = uw->next) {
         if(uw->iblock == iblock) return uw;
@@ -132,7 +132,7 @@ void Frame__set_unwind_target(Frame* self, py_TValue* sp) {
     }
 }
 
-py_TValue* Frame__f_closure_try_get(Frame* self, py_Name name){
+py_TValue* Frame__f_closure_try_get(Frame* self, py_Name name) {
     // if(self->function == NULL) return NULL;
     // pkpy::Function* fn = PyObject__as(pkpy::Function, self->function);
     // if(fn->_closure == nullptr) return nullptr;

+ 13 - 0
src/objects/codeobject.c

@@ -91,4 +91,17 @@ void CodeObject__dtor(CodeObject* self) {
         PK_DECREF(decl);
     }
     c11_vector__dtor(&self->func_decls);
+}
+
+void Function__ctor(Function* self, FuncDecl_ decl, PyObject* module) {
+    PK_INCREF(decl);
+    self->decl = decl;
+    self->module = module;
+    self->clazz = NULL;
+    self->closure = NULL;
+}
+
+void Function__dtor(Function* self) {
+    PK_DECREF(self->decl);
+    if(self->closure) pk_NameDict__delete(self->closure);
 }

+ 14 - 0
src/public/py_object.c

@@ -1,4 +1,5 @@
 #include "pocketpy/interpreter/vm.h"
+#include "pocketpy/common/sstream.h"
 #include "pocketpy/pocketpy.h"
 
 static bool _py_object__new__(int argc, py_Ref argv) {
@@ -29,9 +30,22 @@ static bool _py_object__ne__(int argc, py_Ref argv) {
     return true;
 }
 
+static bool _py_object__repr__(int argc, py_Ref argv) {
+    PY_CHECK_ARGC(1);
+    assert(argv->is_ptr);
+    c11_sbuf buf;
+    c11_sbuf__ctor(&buf);
+    pk_sprintf(&buf, "<%t object at %p>", argv->type, argv->_obj);
+    c11_string* res = c11_sbuf__submit(&buf);
+    py_newstrn(py_retval(), res->data, res->size);
+    c11_string__delete(res);
+    return true;
+}
+
 void pk_object__register() {
     py_bindmagic(tp_object, __new__, _py_object__new__);
     py_bindmagic(tp_object, __hash__, _py_object__hash__);
     py_bindmagic(tp_object, __eq__, _py_object__eq__);
     py_bindmagic(tp_object, __ne__, _py_object__ne__);
+    py_bindmagic(tp_object, __repr__, _py_object__repr__);
 }

+ 21 - 11
src2/main.c

@@ -27,32 +27,42 @@ int main(int argc, char** argv) {
     SetConsoleOutputCP(CP_UTF8);
 #endif
 
-    if(argc > 2){
+    if(argc > 2) {
         printf("Usage: pocketpy [filename]\n");
         return 0;
     }
 
     py_initialize();
 
-    if(argc == 1){
+    if(argc == 1) {
         printf("pocketpy " PK_VERSION " (" __DATE__ ", " __TIME__ ") ");
-        printf("[%d bit] on %s" "\n", (int)(sizeof(void*) * 8), PY_SYS_PLATFORM_STRING);
-        printf("https://github.com/pocketpy/pocketpy" "\n");
-        printf("Type \"exit()\" to exit." "\n");
+        printf(
+            "[%d bit] on %s"
+            "\n",
+            (int)(sizeof(void*) * 8),
+            PY_SYS_PLATFORM_STRING);
+        printf(
+            "https://github.com/pocketpy/pocketpy"
+            "\n");
+        printf(
+            "Type \"exit()\" to exit."
+            "\n");
 
-        while(true){
+        while(true) {
             int size = py_replinput(buf);
             assert(size < sizeof(buf));
-            if(size >= 0){
+            if(size >= 0) {
                 if(!py_exec2(buf, "<stdin>", REPL_MODE)) py_printexc();
             }
         }
-    }else{
+    } else {
         char* source = read_file(argv[1]);
-        if(!py_exec(source)) py_printexc();
-        free(source);
+        if(source) {
+            if(!py_exec(source)) py_printexc();
+            free(source);
+        }
     }
-    
+
     py_finalize();
     return 0;
 }

+ 4 - 4
tests/02_float.py

@@ -1,7 +1,3 @@
-def eq(a, b):
-    dt = a - b
-    return dt > -0.001 and dt < 0.001
-
 # test == != >= <= < >
 assert 1.0 == 1.0
 assert 1.0 != 1.1
@@ -10,6 +6,10 @@ assert 1.0 <= 1.0
 assert 1.0 < 1.1
 assert 1.1 > 1.0
 
+def eq(a, b):
+    dt = a - b
+    return dt > -0.001 and dt < 0.001
+
 # test + - * ** /
 assert eq(1.5 + 3, 4.5)
 assert eq(1.5 + 3.9, 5.4)