py: Add MICROPY_DYNAMIC_COMPILER option to config compiler at runtime.

This new compile-time option allows to make the bytecode compiler
configurable at runtime by setting the fields in the mp_dynamic_compiler
structure.  By using this feature, the compiler can generate bytecode
that targets any MicroPython runtime/VM, regardless of the host and
target compile-time settings.

Options so far that fall under this dynamic setting are:
- maximum number of bits that a small int can hold;
- whether caching of lookups is used in the bytecode;
- whether to use unicode strings or not (lexer behaviour differs, and
  therefore generated string constants differ).
diff --git a/py/compile.c b/py/compile.c
index 7f96891..0699692 100644
--- a/py/compile.c
+++ b/py/compile.c
@@ -2486,7 +2486,23 @@
         // pass
     } else if (MP_PARSE_NODE_IS_SMALL_INT(pn)) {
         mp_int_t arg = MP_PARSE_NODE_LEAF_SMALL_INT(pn);
+        #if MICROPY_DYNAMIC_COMPILER
+        mp_uint_t sign_mask = -(1 << (mp_dynamic_compiler.small_int_bits - 1));
+        if ((arg & sign_mask) == 0 || (arg & sign_mask) == sign_mask) {
+            // integer fits in target runtime's small-int
+            EMIT_ARG(load_const_small_int, arg);
+        } else {
+            // integer doesn't fit, so create a multi-precision int object
+            // (but only create the actual object on the last pass)
+            if (comp->pass != MP_PASS_EMIT) {
+                EMIT_ARG(load_const_obj, mp_const_none);
+            } else {
+                EMIT_ARG(load_const_obj, mp_obj_new_int_from_ll(arg));
+            }
+        }
+        #else
         EMIT_ARG(load_const_small_int, arg);
+        #endif
     } else if (MP_PARSE_NODE_IS_LEAF(pn)) {
         uintptr_t arg = MP_PARSE_NODE_LEAF_ARG(pn);
         switch (MP_PARSE_NODE_LEAF_KIND(pn)) {