From 6c1bf1ff8cc09b82a6f1a0887cc16c841a78dcc6 Mon Sep 17 00:00:00 2001 From: zongor Date: Sat, 18 Oct 2025 16:54:06 -0700 Subject: [PATCH] Fix tests, make auto compile for rom files --- Makefile | 22 +++++-- src/arch/linux/main.c | 142 ++++++++++++++++++++++++++++++++++++++++++ src/tools/assembler.c | 6 +- src/tools/assembler.h | 4 +- src/tools/build.sh | 2 - src/tools/math_gen.c | 31 --------- src/vm/str.c | 37 +++++++++-- test/add.asm.lisp | 14 +++-- test/add.rom | Bin 136 -> 157 bytes test/fib.asm.lisp | 12 ++-- test/fib.rom | Bin 180 -> 201 bytes test/hello.asm.lisp | 23 +++++-- test/hello.rom | Bin 89 -> 142 bytes test/loop.asm.lisp | 22 ++++--- test/loop.rom | Bin 229 -> 250 bytes test/malloc.asm.lisp | 34 ++++++---- test/malloc.rom | Bin 0 -> 185 bytes test/paint-bw.rom | Bin 758 -> 695 bytes test/paint.rom | Bin 1353 -> 1311 bytes test/simple.asm.lisp | 28 +++++---- test/simple.rom | Bin 126 -> 147 bytes test/window.asm.lisp | 9 +-- test/window.rom | Bin 353 -> 367 bytes 23 files changed, 275 insertions(+), 111 deletions(-) delete mode 100644 src/tools/build.sh delete mode 100644 src/tools/math_gen.c create mode 100644 test/malloc.rom diff --git a/Makefile b/Makefile index a076ddf..3c913ca 100644 --- a/Makefile +++ b/Makefile @@ -152,22 +152,34 @@ clean-all: @rm -rf build/ @echo "All cleaned." -# --- HELP --- +# --- TEST COMPILATION TARGET --- +# Compiles all .asm.lisp test files to .rom using the debug VM executable +# Usage: make compile-tests PLATFORM=linux +compile-tests: $(BUILD_DIR)/undar-$(PLATFORM)$(TARGET_SUFFIX) + @echo "Compiling test assembly files for $(PLATFORM)..." + @for f in ./test/*.asm.lisp; do \ + base=$$(basename "$$f" .asm.lisp); \ + echo " [$$base] $$f -> ./test/$$base.rom"; \ + $(BUILD_DIR)/undar-$(PLATFORM)$(TARGET_SUFFIX) "$$f" -o "./test/$$base.rom"; \ + done + @echo "Compilation complete: $$(ls -1 ./test/*.rom | wc -l) ROM files generated" + +# Update help target to include new command help: @echo "Undar VM" @echo "" @echo "Targets:" @echo " make -> debug build (default: linux)" @echo " make debug -> same as above" - @echo " make release -> optimized, stripped build" + @echo " make release -> optimized, stripped build" + @echo " make compile-tests -> compile all test assembly files" @echo " make clean -> clean current platform" - @echo " make clean-all→ clean all platforms" + @echo " make clean-all-> clean all platforms" @echo "" @echo "Platforms:" @echo " make PLATFORM=linux -> GCC + SDL2" @echo " make PLATFORM=emscripten -> Emscripten + SDL2 for Web" - @echo " make PLATFORM=avr -> (example) AVR-GCC" @echo "" @echo "Output:" @echo " Linux: build/linux/undar-linux-" - @echo " Web: build/emscripten/undar.html (+ .js, .wasm)" + @echo " Web: build/emscripten/undar.html (+ .js, .wasm)" \ No newline at end of file diff --git a/src/arch/linux/main.c b/src/arch/linux/main.c index 04eb012..480a99a 100644 --- a/src/arch/linux/main.c +++ b/src/arch/linux/main.c @@ -234,6 +234,144 @@ void repl(VM *vm) { exit(0); } +#ifdef ASM_DEBUG +const char *opcode_to_string(Opcode op) { + static const char *names[] = { + [OP_HALT] = "halt", + [OP_JMP] = "jump", + [OP_JMPF] = "jump-if-flag", + [OP_CALL] = "call", + [OP_RETURN] = "return", + + /* Immediate loads (only 32-bit variant needed) */ + [OP_LOAD_IMM] = "load-immediate", + + /* Register-indirect loads */ + [OP_LOAD_IND_8] = "load-indirect-8", + [OP_LOAD_IND_16] = "load-indirect-16", + [OP_LOAD_IND_32] = "load-indirect-32", + + /* Absolute address loads */ + [OP_LOAD_ABS_8] = "load-absolute-8", + [OP_LOAD_ABS_16] = "load-absolute-16", + [OP_LOAD_ABS_32] = "load-absolute-32", + + /* Base+offset loads */ + [OP_LOAD_OFF_8] = "load-offset-8", + [OP_LOAD_OFF_16] = "load-offset-16", + [OP_LOAD_OFF_32] = "load-offset-32", + + /* Absolute address stores */ + [OP_STORE_ABS_8] = "store-absolute-8", + [OP_STORE_ABS_16] = "store-absolute-16", + [OP_STORE_ABS_32] = "store-absolute-32", + + /* Register-indirect stores */ + [OP_STORE_IND_8] = "store-indirect-8", + [OP_STORE_IND_16] = "store-indirect-16", + [OP_STORE_IND_32] = "store-indirect-32", + + /* Base+offset stores */ + [OP_STORE_OFF_8] = "store-offset-8", + [OP_STORE_OFF_16] = "store-offset-16", + [OP_STORE_OFF_32] = "store-offset-32", + + /* Memory operations */ + [OP_MALLOC] = "malloc", + [OP_MEMSET_8] = "memset-8", + [OP_MEMSET_16] = "memset-16", + [OP_MEMSET_32] = "memset-32", + + /* Stack operations */ + [OP_PUSH] = "push", + [OP_POP] = "pop", + + /* Register operations */ + [OP_REG_MOV] = "register-move", + [OP_SYSCALL] = "syscall", + + /* Bit operations */ + [OP_SLL] = "bit-shift-left", + [OP_SRL] = "bit-shift-right", + [OP_SRE] = "bit-shift-re", + [OP_BAND] = "bit-and", + [OP_BOR] = "bit-or", + [OP_BXOR] = "bit-xor", + + /* Integer arithmetic */ + [OP_ADD_INT] = "add-int", + [OP_SUB_INT] = "sub-int", + [OP_MUL_INT] = "mul-int", + [OP_DIV_INT] = "div-int", + + /* Natural number arithmetic */ + [OP_ADD_NAT] = "add-nat", + [OP_SUB_NAT] = "sub-nat", + [OP_MUL_NAT] = "mul-nat", + [OP_DIV_NAT] = "div-nat", + + /* Floating point operations */ + [OP_ADD_REAL] = "add-real", + [OP_SUB_REAL] = "sub-real", + [OP_MUL_REAL] = "mul-real", + [OP_DIV_REAL] = "div-real", + + /* Type conversions */ + [OP_INT_TO_REAL] = "int-to-real", + [OP_NAT_TO_REAL] = "nat-to-real", + [OP_REAL_TO_INT] = "real-to-int", + [OP_REAL_TO_NAT] = "real-to-nat", + + /* Integer comparisons */ + [OP_JEQ_INT] = "jump-eq-int", + [OP_JNEQ_INT] = "jump-neq-int", + [OP_JGT_INT] = "jump-gt-int", + [OP_JLT_INT] = "jump-lt-int", + [OP_JLE_INT] = "jump-le-int", + [OP_JGE_INT] = "jump-ge-int", + + /* Natural number comparisons */ + [OP_JEQ_NAT] = "jump-eq-nat", + [OP_JNEQ_NAT] = "jump-neq-nat", + [OP_JGT_NAT] = "jump-gt-nat", + [OP_JLT_NAT] = "jump-lt-nat", + [OP_JLE_NAT] = "jump-le-nat", + [OP_JGE_NAT] = "jump-ge-nat", + + /* Floating point comparisons */ + [OP_JEQ_REAL] = "jump-eq-real", + [OP_JNEQ_REAL] = "jump-neq-real", + [OP_JGE_REAL] = "jump-ge-real", + [OP_JGT_REAL] = "jump-gt-real", + [OP_JLT_REAL] = "jump-lt-real", + [OP_JLE_REAL] = "jump-le-real", + + /* String operations */ + [OP_STRLEN] = "string-length", + [OP_STREQ] = "string-eq", + [OP_STRCAT] = "string-concat", + [OP_STR_GET_CHAR] = "string-get-char", + [OP_STR_FIND_CHAR] = "string-find-char", + [OP_STR_SLICE] = "string-slice", + + /* String conversions */ + [OP_INT_TO_STRING] = "int-to-string", + [OP_NAT_TO_STRING] = "nat-to-string", + [OP_REAL_TO_STRING] = "real-to-string", + [OP_STRING_TO_INT] = "string-to-int", + [OP_STRING_TO_NAT] = "string-to-nat", + [OP_STRING_TO_REAL] = "string-to-real" + }; + + if (op < 0 || op >= (int)(sizeof(names) / sizeof(names[0]))) { + return ""; + } + + const char *name = names[op]; + return name ? name : ""; +} +#endif + i32 main(i32 argc, char *argv[]) { bool gui_mode = false; bool dump_rom = false; @@ -397,8 +535,12 @@ i32 main(i32 argc, char *argv[]) { int cycles_this_frame = 0; int max_cycles_per_frame = 1000; // Adjust this value while (cycles_this_frame < max_cycles_per_frame) { + #ifdef ASM_DEBUG + printf("| %s %d\n", opcode_to_string(vm.code[vm.pc]),vm.pc); + #endif if (!step_vm(&vm)) { running = false; + break; } cycles_this_frame++; } diff --git a/src/tools/assembler.c b/src/tools/assembler.c index c9b5305..fa6c645 100644 --- a/src/tools/assembler.c +++ b/src/tools/assembler.c @@ -1,5 +1,5 @@ #include "assembler.h" -typedef enum { SYMBOL_CODE, SYMBOL_DATA, SYMBOL_PLEX } SymbolType; +typedef enum { SYMBOL_CODE, SYMBOL_DATA } SymbolType; typedef struct { char *name; @@ -26,10 +26,6 @@ void symbol_table_add(SymbolTable *table, const char *name, u32 address, // Check for duplicates for (int i = 0; i < table->count; i++) { if (strcmp(table->symbols[i].name, name) == 0) { - // Allow plex redefinition for compiler evolution - if (type == SYMBOL_PLEX && table->symbols[i].type == SYMBOL_PLEX) { - return; - } fprintf(stderr, "Error: Duplicate label '%s'\n", name); exit(1); } diff --git a/src/tools/assembler.h b/src/tools/assembler.h index 9dabb70..7c49cf9 100644 --- a/src/tools/assembler.h +++ b/src/tools/assembler.h @@ -11,9 +11,9 @@ #include #define AS_FIXED(v) ((float)(i32)(v) / 65536.0f) -#define TO_FIXED(f) ((u32)((i32)( \ +#define TO_FIXED(f) ((i32)( \ ((f) >= 0.0f) ? ((f) * 65536.0f + 0.5f) : ((f) * 65536.0f - 0.5f) \ -))) +)) void assemble(VM *vm, ExprNode *program); diff --git a/src/tools/build.sh b/src/tools/build.sh deleted file mode 100644 index b758b83..0000000 --- a/src/tools/build.sh +++ /dev/null @@ -1,2 +0,0 @@ -gcc math_gen.c -o math_gen -lm -./math_gen > ./math.h \ No newline at end of file diff --git a/src/tools/math_gen.c b/src/tools/math_gen.c deleted file mode 100644 index 089d78c..0000000 --- a/src/tools/math_gen.c +++ /dev/null @@ -1,31 +0,0 @@ -#include -#include -#include - -int main() { - printf("#ifndef ZRE_TRIG_TABLES_H\n#define ZRE_TRIG_TABLES_H\n#include \n\n"); - - // Generate SINE table (256 entries, Q16.16 format) - printf("const int32_t sin_table[256] = {\n"); - for (int i = 0; i < 256; i++) { - double angle = i * 2 * M_PI / 256.0; // 0-360° in radians - double value = sin(angle) * 65536.0; // Scale to Q16.16 - int32_t fixed = (int32_t)(value + 0.5); // Round to nearest - printf(" %d,", fixed); - if (i % 8 == 7) printf("\n"); - } - printf("};\n\n"); - - // Generate COSINE table (256 entries, Q16.16 format) - printf("const int32_t cos_table[256] = {\n"); - for (int i = 0; i < 256; i++) { - double angle = i * 2 * M_PI / 256.0; // 0-360° in radians - double value = cos(angle) * 65536.0; // Scale to Q16.16 - int32_t fixed = (int32_t)(value + 0.5); // Round to nearest - printf(" %d,", fixed); - if (i % 8 == 7) printf("\n"); - } - printf("};\n"); - printf("#endif\n"); - return 0; -} \ No newline at end of file diff --git a/src/vm/str.c b/src/vm/str.c index 5f5173d..da0ca1e 100644 --- a/src/vm/str.c +++ b/src/vm/str.c @@ -108,8 +108,9 @@ void fixed_to_string(i32 value, char *buffer) { char temp[32]; i32 negative; u32 int_part; - u32 frac_part, frac_digits; + u32 frac_part, frac_digits, original_frac_digits; char *end = temp + sizeof(temp) - 1; + char *frac_start; *end = '\0'; negative = 0; @@ -122,14 +123,40 @@ void fixed_to_string(i32 value, char *buffer) { frac_part = AS_NAT(value & 0xFFFF); /* Convert fractional part to 5 decimal digits */ - frac_digits = (frac_part * 100000U) / 65536U; + original_frac_digits = (frac_part * 100000U) / 65536U; + frac_digits = original_frac_digits; if (frac_digits > 0) { - end = write_digits_backwards(frac_digits, end, temp); + /* Write fractional digits backwards */ + frac_start = write_digits_backwards(frac_digits, end, temp); + + /* Remove trailing zeros by moving the start pointer */ + while (*(end - 1) == '0' && end > frac_start) { + end--; + } + + /* If all fractional digits were zeros after removing trailing zeros, + we need to add back one zero to represent the .0 */ + if (end == frac_start) { + *--end = '0'; + } + + /* Add decimal point */ *--end = '.'; + } else if (frac_part > 0) { + /* Handle case where original_frac_digits was rounded to 0 but frac_part was not 0 */ + /* This means we have a very small fractional part that should be represented as .0 */ + *--end = '0'; + *--end = '.'; + } else if (frac_part == 0) { + /* No fractional part - just add .0 if we have an integer part */ + if (int_part != 0) { + *--end = '0'; + *--end = '.'; + } } - if (int_part == 0 && frac_digits == 0) { + if (int_part == 0 && frac_digits == 0 && frac_part == 0) { *--end = '0'; } else { end = write_digits_backwards(int_part, end, temp); @@ -140,4 +167,4 @@ void fixed_to_string(i32 value, char *buffer) { } strcopy(buffer, end, temp + sizeof(temp) - end); -} +} \ No newline at end of file diff --git a/test/add.asm.lisp b/test/add.asm.lisp index 9bd95bf..802ac5f 100644 --- a/test/add.asm.lisp +++ b/test/add.asm.lisp @@ -8,7 +8,7 @@ (pop $0) (int-to-string $1 $0) (push $1) - (call &println) + (call &pln) (halt)) (label add @@ -18,15 +18,17 @@ (push $2) (return)) - (label println - (load-immediate $0 &terminal-namespace) - (syscall OPEN $0 $0 $0) + (label pln + (load-immediate $0 &terminal-namespace) ; get terminal device + (load-immediate $11 0) + (syscall OPEN $0 $0 $11) (load-immediate $3 &new-line) (pop $1) + (load-offset-32 $7 $0 4) ; load handle (string-length $2 $1) - (syscall WRITE $0 $1 $2) + (syscall WRITE $7 $1 $2) (string-length $4 $3) - (syscall WRITE $0 $3 $4) + (syscall WRITE $7 $3 $4) (return))) (data (label terminal-namespace "/dev/term/0") diff --git a/test/add.rom b/test/add.rom index 3a769ffb9da37129ff8f2151982febf36c7c4908..d9dcc0ad8e78753535cc33c283bd3f948df53446 100644 GIT binary patch literal 157 zcmYj|y9$Ir3`K7qgP_hzcXclb5L%Bv zZKOtQf?jOShY?XGrK^YR2vwd#{sXFDEfJ+D;0&wEU}Pb>y8WWL*IcFjxYPIeTxo&Q FOFU-l2YUbj literal 136 zcmZQzU|k)`~$iub_O0=`B>(^b diff --git a/test/loop.asm.lisp b/test/loop.asm.lisp index 6f13625..9d91348 100644 --- a/test/loop.asm.lisp +++ b/test/loop.asm.lisp @@ -27,16 +27,18 @@ (push $3) (call &pln) (halt)) - (label pln - (load-immediate $0 &terminal-namespace) - (syscall OPEN $0 $0 $0) - (load-immediate $3 &new-line) - (pop $1) - (string-length $2 $1) - (syscall WRITE $0 $1 $2) - (string-length $4 $3) - (syscall WRITE $0 $3 $4) - (return))) + (label pln + (load-immediate $0 &terminal-namespace) ; get terminal device + (load-immediate $11 0) + (syscall OPEN $0 $0 $11) + (load-immediate $3 &new-line) + (pop $1) + (load-offset-32 $7 $0 4) ; load handle + (string-length $2 $1) + (syscall WRITE $7 $1 $2) + (string-length $4 $3) + (syscall WRITE $7 $3 $4) + (return))) (data (label terminal-namespace "/dev/term/0") (label help "Enter a string: ") diff --git a/test/loop.rom b/test/loop.rom index 70e8b578ee33cd49ae801f4fa6710132ff10fae5..60fb07cfc882a4b0da5733fd2ebe7258b1d6e26e 100644 GIT binary patch literal 250 zcmYk0u?oUK5JY!xZ@D8UqS#nOlSUiC*w|<-q|;vz3}Ru582twS#!u9_*ks^#-!RK? zL_{CFzE(Y>U?t`E1lz`<%E;4}ELnw(1mNxnY``2hev zlJhvu1Pf=t_+A6B(FN02f-{}(q-+qW-pT?E7kY3&;Cw02xV$3iMef)Ifko*m{ZSZK zdP7gavqf(cwFI6Lftsq(Ug`=PgXxVJG?xBI*6LC0`d6I#@g8gNqT4}4xP-?v-iGTb HKyrg09w8KE diff --git a/test/malloc.asm.lisp b/test/malloc.asm.lisp index 5ace03f..253c274 100644 --- a/test/malloc.asm.lisp +++ b/test/malloc.asm.lisp @@ -1,25 +1,33 @@ ((code (label main (load-immediate $0 &terminal-namespace) ; get terminal device + (load-immediate $11 0) + (syscall OPEN $0 $0 $11) + (load-immediate $1 &help) ; print help message + (push $0) (push $1) (call &pln) + (load-immediate $1 32) ; read in a string of max 32 char length - (malloc $2 $1) ; allocate memory for the string - (syscall READ $0 $3 $1 $2) ; read the string - (push $3) + (malloc $4 $1) ; allocate memory for the string + (load-offset-32 $7 $0 4) ; load handle + (syscall READ $7 $2 $1 $4) ; read the string + + (push $0) + (push $4) (call &pln) ; print the string (halt)) - (label pln - (load-immediate $0 &terminal-namespace) - (syscall OPEN $0 $0 $0) - (load-immediate $3 &new-line) - (pop $1) - (string-length $2 $1) - (syscall WRITE $0 $1 $2) - (string-length $4 $3) - (syscall WRITE $0 $3 $4) - (return))) + (label pln + (load-immediate $3 &new-line) + (pop $1) + (pop $0) + (load-offset-32 $7 $0 4) ; load handle + (string-length $2 $1) + (syscall WRITE $7 $1 $2) + (string-length $4 $3) + (syscall WRITE $7 $3 $4) + (return))) (data (label terminal-namespace "/dev/term/0") (label help "Enter a string: ") diff --git a/test/malloc.rom b/test/malloc.rom new file mode 100644 index 0000000000000000000000000000000000000000..8a8492d3f4c2da4a4bd0a2fac13c9f0fc7749eb9 GIT binary patch literal 185 zcmYj}u?oUK5JYEh_Xrpf?JN@R8Y_#lwpa2Sf=IkZM{lTSA9U^h)p&se6-610w}TX`C1vRvYkoVAe~Ie`jzKwA926rax4(HKTMN bKWQGPS2|%wf5|lQ7T@#q%#U+KdB4IJdw&gT literal 0 HcmV?d00001 diff --git a/test/paint-bw.rom b/test/paint-bw.rom index 1758b2d8f2a5ef561e8e3d71d3beaca556ad5453..0bde7ec34573306040f3f6709eea0198e7e6e638 100644 GIT binary patch literal 695 zcma)3y-EW?5Z=GJy=3op&m$KI%qE3!YAhlMA)=i&*7^pTQU!y)g%(ymhlTiRR&i!c zAZVdm%s2DR-*W^ASYVD8rM~=+7W~<<7U%lgv=JnqjrU@ohUO?~@FxwHtWrc5MU+uR z9ihz>v{u8>6W>D35@acoIz`e#mxqR~Gt{A|vlkm@PKk3VGkv4puAF`l7~{+_WdQio zA=_eW`B+JgL4_ItA<1cTW!sLnDJ4l~2Q$o4%0wTFSdvs;*??quyP;HV2yPsj00aK5 z;}8Zz6Ncd1%oIaw238lXF=_`u&h}_8ERCna$@TnkvbegR&u=FOqFggKcMpp>5lbRA O-#R{DkuNKJU*Q+vF)%#< literal 758 zcma)3JB}1F5N+F4cDvjCnV#wLbk@LR1T)%|5E3B~F-KrW+GH1r6*%qHT-CAT*2B|l14Rdtb5u5*!1w%KKzy~Q~=y#67}vRCT7BI6X9a1b(` zu#*0%s(}kFH`wGc!fWQD-W+~$T4fHu*#Sg&#W#qu5cDgtK~}5EfGjyeLRB`GHq6ll zy)rC0k|(cMkXzQ!vMqTWn_!X&Yc2T$%yw6GU@$ v%X9GrEtl?Zj-U1q?>-!l_xnR3Kc~#y`;QMtCQeLTo-=;4kY5-7eer()jQ@A--yCHf&e^8$j%WCCtcQ3tNfW zV6Ph08DU%(2#`Z;=S?kPSYt0Pc0*eY9WC~aA6dCeJqZPE79Qvbw_dvo!pM_ebh zjpM~r@bONMIN*rxy!{bJTN;;Xe5S?@er3w4)98;_ZE4)>)p*MhokoAehb@h}y&9jl zG|r0Xip7r4V-!d#W4cv8#!Jq;#EX83^(3ikM6f{w8C1{#+C)K{YPc3~TB!YqB#xwx zBbma0HgyAQFhXJEC^XjGL7$B?d$%xlAomUt##nQJGXPYIkes77rQ(h07_Yqc;31~d zW-4uSoyIANdFDRxNt`mF3PmU}Rg!E#($l%*O4T}N=dN)up=UOCZZb7)3RRn!Vw##s zs*BbbbrC@Bf5L>|#8kL&ac}u#u{wIRTs~M_5oPDO|M2l@iH;L=Y+5(Iz|PyuH_VUB zr_686*UUf6oJoHOGJ2(C^n}U$8sa^ZJ`@=y9&NzfVLoS4{gEB7A$~BQF~2hpnXi~H Mn7^1`n7^5S0Rz)jp#T5? literal 1353 zcma)+y>8S%5Xbjx*7o^h&H21OOB4pKK`HK(NHmBl1wm9iL%5C)DJQ7;XlNoKc!0b@ zq)5C34Ud4x6ChC{!pzZS8+j!vcT>QY7EB0B@zky-J;g-j3%CGdO@V!%4y~nU(wdHYd(BmU3 zdXHhn*_OwnL66T{9#<*)@k#tGRDq;{r#bK*KXU9Pe&{p&(2^!dG-)Fm*{DW0&{+jt zq+#zqZVR;=U_&N#$YcZ~`q6BZ8dNA$PMSI~53!bnoy8N>j^yG9VN7E7aR-1#5$rWu zBO2$>j`7NC4>;!3P8X}FOLP>rB=(tj>uuOFP0?gcj;)eq4U!JiO_!_Axpd}g2W>dM z&b3qHrciX&WYfr0k=1*qAq<;y6aLDp>KA&pp-`J5A)Jd-IEs43g2-ZO?*lq}B{fzVv*RG0XM%4o~ahg%{j|c;k zMICWN^@E1UG_Uf(y5MDar&*wGNJ~qnSc1}iI`QH^zF8*L86{g?sbtf8TULvz2?fd`aTh*7f7#I=QsyvK(eF4oG_@y;ubc#C>%cxyL27S+fz!1VF|ByvOiwV UiGl3Sk*{>xvzNcVz%Q$uKX#`b)&Kwi literal 353 zcmYLEK~BRk5VXBsCrvikahzNu!~+82#GwZyegc+yX^~pm&-O88*DZ7pGrQyQ*sBzm z7P!~@y;W3tYK2nqwG@P@4~$*x6UteLq{TZAQM)vduK$K<=*B65n8t}hWF`=6XW;{F zk8tK88HQlM5|$W;rdqj|4Xdk`&@#`gxyliiXc=nv=<76lU(J5>q>IaYjqt{P@8SPh z>}~;7a}8};UhRoRf>*|<*G6C6+N|yEB3S#ths#$!Er-v2&b#GVNm5a^-^W8{LH=8W PpS<7lESogy!YK6%#g`ZU