From 12a5646d5669c8a27181b97dde8556fa4f18d43b Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 29 Sep 2025 02:36:57 +0200 Subject: [PATCH] Added some crypto 100% implemented in Fun. (0.11.0) --- CMakeLists.txt | 2 +- examples/crc32_demo.fun | 32 +++ examples/md5_demo.fun | 34 +++ examples/sha256_demo.fun | 33 +++ examples/sha512_demo.fun | 35 +++ examples/sha512_str_demo.fun | 34 +++ lib/crypt/crc32.fun | 187 +++++++++++++ lib/crypt/md5.fun | 519 +++++++++++++++++++++++++++++++++++ lib/crypt/sha256.fun | 261 ++++++++++++++++++ lib/crypt/sha512.fun | 419 ++++++++++++++++++++++++++++ src/bytecode.h | 12 +- src/parser.c | 109 +++++++- src/parser_utils.c | 4 + src/vm.c | 28 +- src/vm.h | 5 +- src/vm/arrays/index_get.c | 3 +- src/vm/bitwise/band.c | 27 ++ src/vm/bitwise/bnot.c | 24 ++ src/vm/bitwise/bor.c | 27 ++ src/vm/bitwise/bxor.c | 27 ++ src/vm/bitwise/rol.c | 28 ++ src/vm/bitwise/ror.c | 28 ++ src/vm/bitwise/shl.c | 28 ++ src/vm/bitwise/shr.c | 28 ++ src/vm/len.c | 4 +- 25 files changed, 1926 insertions(+), 12 deletions(-) create mode 100644 examples/crc32_demo.fun create mode 100644 examples/md5_demo.fun create mode 100644 examples/sha256_demo.fun create mode 100644 examples/sha512_demo.fun create mode 100644 examples/sha512_str_demo.fun create mode 100644 lib/crypt/crc32.fun create mode 100644 lib/crypt/md5.fun create mode 100644 lib/crypt/sha256.fun create mode 100644 lib/crypt/sha512.fun create mode 100644 src/vm/bitwise/band.c create mode 100644 src/vm/bitwise/bnot.c create mode 100644 src/vm/bitwise/bor.c create mode 100644 src/vm/bitwise/bxor.c create mode 100644 src/vm/bitwise/rol.c create mode 100644 src/vm/bitwise/ror.c create mode 100644 src/vm/bitwise/shl.c create mode 100644 src/vm/bitwise/shr.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5837cbd..2894f35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.10.0 LANGUAGES C) +project(fun VERSION 0.11.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/crc32_demo.fun b/examples/crc32_demo.fun new file mode 100644 index 0000000..1a8df56 --- /dev/null +++ b/examples/crc32_demo.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// Include the pure Fun CRC32 library +include "lib/crypt/crc32.fun" + +// Create a Class-typed instance to prevent accidental shadowing +Class c32 = CRC32() +print(typeof(c32)) // should be "Class" + +print("=== CRC32 demo (hex input) ===") + +// "" (empty) -> 00000000 +print(c32.crc32_hex("")) + + // "abc" -> 352441c2 +print(c32.crc32_hex("616263")) + +// "123456789" -> cbf43926 +print(c32.crc32_hex("313233343536373839")) + +print("=== Done ===") diff --git a/examples/md5_demo.fun b/examples/md5_demo.fun new file mode 100644 index 0000000..96b0a30 --- /dev/null +++ b/examples/md5_demo.fun @@ -0,0 +1,34 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// Include the pure Fun MD5 library +include "lib/crypt/md5.fun" + +// create a hasher instance +md5 = MD5() + +print("=== MD5 demo (hex input) ===") + +// "abc" => 0x61 0x62 0x63 +print(md5.md5_hex("616263")) // -> 900150983cd24fb0d6963f7d28e17f72 + +// empty string "" => hex "" +print(md5.md5_hex("")) // -> d41d8cd98f00b204e9800998ecf8427e + +// "message digest" +print(md5.md5_hex("6d65737361676520646967657374")) // -> f96b697d7cb7938d525a2f31aaf161d0 + +// "abcdefghijklmnopqrstuvwxyz" +print(md5.md5_hex("6162636465666768696a6b6c6d6e6f707172737475767778797a")) // -> c3fcd3d76192e4007dfb496cca67e13b + +print("=== Done ===") diff --git a/examples/sha256_demo.fun b/examples/sha256_demo.fun new file mode 100644 index 0000000..03eac10 --- /dev/null +++ b/examples/sha256_demo.fun @@ -0,0 +1,33 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// Include the pure Fun SHA-256 library +include "lib/crypt/sha256.fun" + +sha = SHA256() + +print("=== SHA-256 demo (hex input) ===") + +// "" (empty) +print(sha.sha256_hex("")) // -> e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + +// "abc" +print(sha.sha256_hex("616263")) // -> ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad + +// "message digest" +print(sha.sha256_hex("6d65737361676520646967657374")) // -> f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650 + +// "abcdefghijklmnopqrstuvwxyz" +print(sha.sha256_hex("6162636465666768696a6b6c6d6e6f707172737475767778797a")) // -> 71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73 + +print("=== Done ===") diff --git a/examples/sha512_demo.fun b/examples/sha512_demo.fun new file mode 100644 index 0000000..c693fbe --- /dev/null +++ b/examples/sha512_demo.fun @@ -0,0 +1,35 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +include "lib/crypt/sha512.fun" + +sha = SHA512() + +print("=== SHA-512 demo (hex input) ===") + +// "" (empty) +print(sha.sha512_hex("")) // -> cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce + // 47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e + +// "abc" +print(sha.sha512_hex("616263")) // -> ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a + // 2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f + +// "message digest" +print(sha.sha512_hex("6d65737361676520646967657374")) +// -> 107dbf389d9e9f71a3a95f6c055b9251 +// bc5268c2be16d6c13492ea45b0199f33 +// 09e16455ab1e96118e8a905d5597b721 +// 3f36cc64dab6c0c5 + +print("=== Done ===") diff --git a/examples/sha512_str_demo.fun b/examples/sha512_str_demo.fun new file mode 100644 index 0000000..1518f62 --- /dev/null +++ b/examples/sha512_str_demo.fun @@ -0,0 +1,34 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// Demo: SHA-512 of raw strings (no hex decoding) +include "lib/crypt/sha512.fun" + +sha = SHA512() + +print("=== SHA-512 demo (raw string input via sha512_str) ===") + +// "" (empty string) +print(sha.sha512_str("")) // -> cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce + // 47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e + +// "abc" +print(sha.sha512_str("abc")) // -> ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a + // 2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f + +// "616263" (the literal characters '6','1','6','2','6','3') +print(sha.sha512_str("616263")) // -> ee21095236a9c037f705f41ffa3cf60869891fcdb461c5c8fe50e5b1711a27b + // fc02de2e387228651bdd034113cc59af777fdb9c915b70fdbed0eeacf7113296b + +print("=== Note ===") +print("sha512_str(\"616263\") hashes the text 616263; sha512_hex(\"616263\") hashes bytes 0x61 0x62 0x63 (abc).") diff --git a/lib/crypt/crc32.fun b/lib/crypt/crc32.fun new file mode 100644 index 0000000..fe44063 --- /dev/null +++ b/lib/crypt/crc32.fun @@ -0,0 +1,187 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// lib/crypt/crc32.fun +// Pure Fun CRC32 (IEEE 802.3, polynomial 0xEDB88320, reflected) operating on hex-string input. +// +// Public API (class): +// crc = CRC32() +// crc.crc32_hex(hexStr) -> digest hex string (lowercase, 8 hex chars) +// +// Example: +// print(CRC32().crc32_hex("616263")) // "abc" => 352441c2 + +class CRC32() + // hex helpers + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + // ensure we operate on a string + hex = to_string(hex) + arr = [] + number i = 0 + while true + ch1 = substr(hex, i, 1) + if (typeof(ch1) != "String") + break + if (ch1 == "") + break + ch2 = substr(hex, i + 1, 1) + if (typeof(ch2) != "String") + break + if (ch2 == "") + break + pair = join([ch1, ch2], "") + number b = this.byte_from_hex_pair(pair) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + hexd = "0123456789abcdef" + number hi = n / 16 + number lo = n % 16 + ch1 = substr(hexd, hi, 1) + ch2 = substr(hexd, lo, 1) + return join([ch1, ch2], "") + + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + // Core per-byte CRC (bit-by-bit, reflected), polynomial 0xEDB88320 + fun crc32_bytes(this, bytes) + crc = 4294967295 // 0xFFFFFFFF initial + poly = 3988292384 // 0xEDB88320 + number i = 0 + L = len(bytes) + while i < L + // crc ^= byte + crc = bxor(crc, bytes[i]) + // 8 times + j = 0 + while j < 8 + if (band(crc, 1) == 1) + // crc = (crc >> 1) ^ poly + crc = bxor(shr(crc, 1), poly) + else + // crc >>= 1 + crc = shr(crc, 1) + j = j + 1 + i = i + 1 + // final xor + crc = bxor(crc, 4294967295) + return this.u32(crc) + + fun crc32_hex(this, hexStr) + // Process hex input directly with only core built-ins; no method calls on 'this' + hex = to_string(hexStr) + hexd = "0123456789abcdef" + + // CRC state + number crc = 4294967295 // 0xFFFFFFFF + number poly = 3988292384 // 0xEDB88320 + + // Iterate two hex nibbles at a time + number i = 0 + while true + ch1 = substr(hex, i, 1) + if (typeof(ch1) != "String" || ch1 == "") + break + ch2 = substr(hex, i + 1, 1) + if (typeof(ch2) != "String" || ch2 == "") + break + + // Convert two hex chars to a byte without helper calls + // find returns index of substring or -1; ensure lowercase + h1 = find(hexd, to_string(ch1)) + h2 = find(hexd, to_string(ch2)) + if (h1 < 0 || h2 < 0) + // invalid hex -> stop + break + number b = h1 * 16 + h2 + + // crc ^= b + crc = bxor(crc, b) + // 8 rounds + number j = 0 + while j < 8 + if (band(crc, 1) == 1) + crc = bxor(shr(crc, 1), poly) + else + crc = shr(crc, 1) + j = j + 1 + + i = i + 2 + + // final xor + crc = bxor(crc, 4294967295) + + // Format result as 8 hex chars big-endian without calling this.two_hex + number b3 = (crc / 16777216) % 256 + number b2 = (crc / 65536) % 256 + number b1 = (crc / 256) % 256 + number b0 = crc % 256 + + // encode a single byte to two hex chars using hexd and substr + fun b2hex(byte) + number hi = (byte / 16) % 16 + number lo = (byte % 16) + c1 = substr(hexd, hi, 1) + c2 = substr(hexd, lo, 1) + return join([c1, c2], "") + + parts = [ b2hex(b3), b2hex(b2), b2hex(b1), b2hex(b0) ] + return join(parts, "") diff --git a/lib/crypt/md5.fun b/lib/crypt/md5.fun new file mode 100644 index 0000000..81474c7 --- /dev/null +++ b/lib/crypt/md5.fun @@ -0,0 +1,519 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// lib/crypt/md5.fun +// Pure Fun implementation of MD5 operating on hex-string input. +// +// Public API: +// md5_hex(hexStr) -> digest hex string (lowercase) +// +// Example: +// print(md5_hex("616263")) // "abc" => 900150983cd24fb0d6963f7d28e17f72 + +/* Legacy global API and helpers are commented out below. + Please use the MD5 class (see at the end of this file). + +fun u32(x) + // bring x into [0 .. 2^32-1] by modular reduction + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + +fun shl32(x, s) + return shl(u32(x), s) + +fun shr32(x, s) + return shr(u32(x), s) + +fun rol32(x, s) + return rol(u32(x), s) + +fun and32(a, b) + return band(u32(a), u32(b)) + +fun or32(a, b) + return bor(u32(a), u32(b)) + +fun xor32(a, b) + return bxor(u32(a), u32(b)) + +fun not32(x) + return bnot(u32(x)) + +fun add32(a, b) + return u32(a + b) + +fun add32_4(a, b, c, d) + return u32(u32(u32(a + b) + c) + d) + +fun hex_val(ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + +fun byte_from_hex_pair(hh) + // hh is 2-char string + hi = hex_val(substr(hh, 0, 1)) + lo = hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + +fun from_hex(hex) + // parse even-length hex string to array of bytes (numbers 0..255) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + b = byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + +fun two_hex(n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + // build with join + parts = [d[hi], d[lo]] + return join(parts, "") + +fun bytes_to_hex(arr) + i = 0 + out = [] + while i < len(arr) + push(out, two_hex(arr[i])) + i = i + 1 + return join(out, "") + +// MD5 auxiliary functions +fun F(x, y, z) + return or32(and32(x, y), and32(not32(x), z)) + +fun G(x, y, z) + return or32(and32(x, z), and32(y, not32(z))) + +fun H(x, y, z) + return xor32(xor32(x, y), z) + +fun I(x, y, z) + return xor32(y, or32(x, not32(z))) + +// T constants (K) = floor(abs(sin(i+1)) * 2^32), i=0..63 +K = [ + 3614090360, 3905402710, 606105819, 3250441966, 4118548399, 1200080426, 2821735955, 4249261313, + 1770035416, 2336552879, 4294925233, 2304563134, 1804603682, 4254626195, 2792965006, 1236535329, + 4129170786, 3225465664, 643717713, 3921069994, 3593408605, 38016083, 3634488961, 3889429448, + 568446438, 3275163606, 4107603335, 1163531501, 2850285829, 4243563512, 1735328473, 2368359562, + 4294588738, 2272392833, 1839030562, 4259657740, 2763975236, 1272893353, 4139469664, 3200236656, + 681279174, 3936430074, 3572445317, 76029189, 3654602809, 3873151461, 530742520, 3299628645, + 4096336452, 1126891415, 2878612391, 4237533241, 1700485571, 2399980690, 4293915773, 2240044497, + 1873313359, 4264355552, 2734768916, 1309151649, 4149444226, 3174756917, 718787259, 3951481745 +] + +// per-round rotation amounts +S = [ + 7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22, + 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, + 4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23, + 6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21 +] + +fun pad_bytes(bytes) + // returns a new array with MD5 padding + L = len(bytes) + out = [] + // copy original bytes + i = 0 + while i < L + push(out, bytes[i]) + i = i + 1 + // append 0x80 + push(out, 128) + // pad zeros until length % 64 == 56 + while (len(out) % 64) != 56 + push(out, 0) + // append length (bits) as 64-bit little-endian + len_bits = L * 8 + j = 0 + while j < 8 + b = (len_bits / pow(2, 8 * j)) % 256 + push(out, b) + j = j + 1 + return out + +fun word32_le(b0, b1, b2, b3) + return u32(b0 + b1 * 256 + b2 * 65536 + b3 * 16777216) + +fun process_block(state, block) + // state: [a0,b0,c0,d0], block: 64 bytes array + a0 = state[0] + b0 = state[1] + c0 = state[2] + d0 = state[3] + + // M[16] little-endian words + M = [] + i = 0 + while i < 16 + j = i * 4 + w = word32_le(block[j], block[j+1], block[j+2], block[j+3]) + push(M, w) + i = i + 1 + + A = a0 + B = b0 + C = c0 + D = d0 + + k = 0 + while k < 64 + if (k < 16) + f = F(B, C, D) + g = k + else if (k < 32) + f = G(B, C, D) + g = (5 * k + 1) % 16 + else if (k < 48) + f = H(B, C, D) + g = (3 * k + 5) % 16 + else + f = I(B, C, D) + g = (7 * k) % 16 + + tmp = D + sum = add32_4(A, f, K[k], M[g]) + D = C + C = B + B = add32(B, rol32(sum, S[k])) + A = tmp + k = k + 1 + + state[0] = add32(state[0], A) + state[1] = add32(state[1], B) + state[2] = add32(state[2], C) + state[3] = add32(state[3], D) + return state + +fun md5_bytes(bytes) + // returns array of 16 bytes of digest + data = pad_bytes(bytes) + // initial state + a0 = 1732584193 // 0x67452301 + b0 = 4023233417 // 0xEFCDAB89 + c0 = 2562383102 // 0x98BADCFE + d0 = 271733878 // 0x10325476 + state = [a0, b0, c0, d0] + + off = 0 + N = len(data) + while off < N + block = [] + i = 0 + while i < 64 + push(block, data[off + i]) + i = i + 1 + state = process_block(state, block) + off = off + 64 + + // output digest as little-endian bytes of A,B,C,D + out = [] + j = 0 + while j < 4 + v = state[j] + // 4 bytes little-endian + push(out, (v ) % 256) + push(out, (v / 256) % 256) + push(out, (v / 65536) % 256) + push(out, (v / 16777216) % 256) + j = j + 1 + return out + +fun md5_hex(hexStr) + bytes = from_hex(hexStr) + digest = md5_bytes(bytes) + return bytes_to_hex(digest) + +// Class wrapper to avoid global name collisions and show class usage +*/ +class MD5() + // MD5 constants (as fields) + K = [ + 3614090360, 3905402710, 606105819, 3250441966, 4118548399, 1200080426, 2821735955, 4249261313, + 1770035416, 2336552879, 4294925233, 2304563134, 1804603682, 4254626195, 2792965006, 1236535329, + 4129170786, 3225465664, 643717713, 3921069994, 3593408605, 38016083, 3634488961, 3889429448, + 568446438, 3275163606, 4107603335, 1163531501, 2850285829, 4243563512, 1735328473, 2368359562, + 4294588738, 2272392833, 1839030562, 4259657740, 2763975236, 1272893353, 4139469664, 3200236656, + 681279174, 3936430074, 3572445317, 76029189, 3654602809, 3873151461, 530742520, 3299628645, + 4096336452, 1126891415, 2878612391, 4237533241, 1700485571, 2399980690, 4293915773, 2240044497, + 1873313359, 4264355552, 2734768916, 1309151649, 4149444226, 3174756917, 718787259, 3951481745 + ] + S = [ + 7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22, + 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, + 4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23, + 6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21 + ] + + // 32-bit helpers + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + fun shl32(this, x, s) + return shl(this.u32(x), s) + + fun shr32(this, x, s) + return shr(this.u32(x), s) + + fun rol32(this, x, s) + return rol(this.u32(x), s) + + fun and32(this, a, b) + return band(this.u32(a), this.u32(b)) + + fun or32(this, a, b) + return bor(this.u32(a), this.u32(b)) + + fun xor32(this, a, b) + return bxor(this.u32(a), this.u32(b)) + + fun not32(this, x) + return bnot(this.u32(x)) + + // tiny adders + fun add32(this, a, b) + return this.u32(a + b) + + fun add32_4(this, a, b, c, d) + return this.u32(this.u32(this.u32(a + b) + c) + d) + + // hex helpers + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + b = this.byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + parts = [d[hi], d[lo]] + return join(parts, "") + + fun bytes_to_hex(this, arr) + i = 0 + out = [] + while i < len(arr) + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + // MD5 aux + fun F(this, x, y, z) + return this.or32(this.and32(x, y), this.and32(this.not32(x), z)) + + fun G(this, x, y, z) + return this.or32(this.and32(x, z), this.and32(y, this.not32(z))) + + fun H(this, x, y, z) + return this.xor32(this.xor32(x, y), z) + + fun I(this, x, y, z) + return this.xor32(y, this.or32(x, this.not32(z))) + + fun pad_bytes(this, bytes) + L = len(bytes) + out = [] + i = 0 + while i < L + push(out, bytes[i]) + i = i + 1 + push(out, 128) + while (len(out) % 64) != 56 + push(out, 0) + len_bits = L * 8 + j = 0 + while j < 8 + b = (len_bits / pow(2, 8 * j)) % 256 + push(out, b) + j = j + 1 + return out + + fun word32_le(this, b0, b1, b2, b3) + return this.u32(b0 + b1 * 256 + b2 * 65536 + b3 * 16777216) + + fun process_block(this, state, block) + a0 = state[0] + b0 = state[1] + c0 = state[2] + d0 = state[3] + + M = [] + i = 0 + while i < 16 + j = i * 4 + w = this.word32_le(block[j], block[j+1], block[j+2], block[j+3]) + push(M, w) + i = i + 1 + + A = a0 + B = b0 + C = c0 + D = d0 + + k = 0 + while k < 64 + if (k < 16) + f = this.F(B, C, D) + g = k + else if (k < 32) + f = this.G(B, C, D) + g = (5 * k + 1) % 16 + else if (k < 48) + f = this.H(B, C, D) + g = (3 * k + 5) % 16 + else + f = this.I(B, C, D) + g = (7 * k) % 16 + + tmp = D + sum = this.add32_4(A, f, this.K[k], M[g]) + D = C + C = B + B = this.add32(B, this.rol32(sum, this.S[k])) + A = tmp + k = k + 1 + + state[0] = this.add32(state[0], A) + state[1] = this.add32(state[1], B) + state[2] = this.add32(state[2], C) + state[3] = this.add32(state[3], D) + return state + + fun md5_bytes(this, bytes) + data = this.pad_bytes(bytes) + a0 = 1732584193 + b0 = 4023233417 + c0 = 2562383102 + d0 = 271733878 + state = [a0, b0, c0, d0] + + off = 0 + N = len(data) + while off < N + block = [] + i = 0 + while i < 64 + push(block, data[off + i]) + i = i + 1 + state = this.process_block(state, block) + off = off + 64 + + out = [] + j = 0 + while j < 4 + v = state[j] + push(out, (v) % 256) + push(out, (v / 256) % 256) + push(out, (v / 65536) % 256) + push(out, (v / 16777216) % 256) + j = j + 1 + return out + + fun md5_hex(this, hexStr) + bytes = this.from_hex(hexStr) + digest = this.md5_bytes(bytes) + return this.bytes_to_hex(digest) diff --git a/lib/crypt/sha256.fun b/lib/crypt/sha256.fun new file mode 100644 index 0000000..f297972 --- /dev/null +++ b/lib/crypt/sha256.fun @@ -0,0 +1,261 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// lib/crypt/sha256.fun +// Pure Fun SHA-256 implementation operating on hex-string input. +// +// Public API (class): +// sha = SHA256() +// sha.sha256_hex(hexStr) -> digest hex string (lowercase) +// +// Example: +// print(SHA256().sha256_hex("616263")) // "abc" => ba7816bf... + +class SHA256() + // 32-bit helpers + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + fun add32(this, a, b) + return this.u32(a + b) + + fun add32_5(this, a, b, c, d, e) + return this.u32(this.u32(this.u32(this.u32(a + b) + c) + d) + e) + + // bitwise (use native VM ops) + fun rotr32(this, x, s) + return ror(this.u32(x), s) + + fun shr32(this, x, s) + return shr(this.u32(x), s) + + fun ch(this, x, y, z) + // (x & y) ^ (~x & z) + return bxor(band(x, y), band(bnot(x), z)) + + fun maj(this, x, y, z) + // (x & y) ^ (x & z) ^ (y & z) + return bxor(bxor(band(x, y), band(x, z)), band(y, z)) + + fun bigSigma0(this, x) + // ROTR2 ^ ROTR13 ^ ROTR22 + return bxor(bxor(this.rotr32(x, 2), this.rotr32(x, 13)), this.rotr32(x, 22)) + + fun bigSigma1(this, x) + // ROTR6 ^ ROTR11 ^ ROTR25 + return bxor(bxor(this.rotr32(x, 6), this.rotr32(x, 11)), this.rotr32(x, 25)) + + fun smallSigma0(this, x) + // ROTR7 ^ ROTR18 ^ SHR3 + return bxor(bxor(this.rotr32(x, 7), this.rotr32(x, 18)), this.shr32(x, 3)) + + fun smallSigma1(this, x) + // ROTR17 ^ ROTR19 ^ SHR10 + return bxor(bxor(this.rotr32(x, 17), this.rotr32(x, 19)), this.shr32(x, 10)) + + // constants + IV = [ + 1779033703, 3144134277, 1013904242, 2773480762, + 1359893119, 2600822924, 528734635, 1541459225 + ] + + K = [ + 1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, + 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580, + 3835390401, 4022224774, 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, + 2554220882, 2821834349, 2952996808, 3210313671, 3336571891, 3584528711, 113926993, 338241895, + 666307205, 773529912, 1294757372, 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, + 2730485921, 2820302411, 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, + 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, + 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, 3329325298 + ] + + // hex helpers + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + b = this.byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + parts = [d[hi], d[lo]] + return join(parts, "") + + fun bytes_to_hex(this, arr) + i = 0 + out = [] + while i < len(arr) + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + // padding (SHA-256) + fun pad_bytes(this, bytes) + L = len(bytes) + out = [] + i = 0 + while i < L + push(out, bytes[i]) + i = i + 1 + // append 0x80 + push(out, 128) + // zeros until length % 64 == 56 + while (len(out) % 64) != 56 + push(out, 0) + // length in bits as 64-bit big-endian + len_bits = L * 8 + j = 7 + while j >= 0 + b = (len_bits / pow(2, 8 * (7 - j))) % 256 + push(out, b) + j = j - 1 + return out + + fun word32_be(this, b0, b1, b2, b3) + // big-endian + return this.u32(b0 * 16777216 + b1 * 65536 + b2 * 256 + b3) + + fun process_block(this, state, block) + // prepare message schedule W[64] + W = [] + t = 0 + while t < 16 + j = t * 4 + push(W, this.word32_be(block[j], block[j+1], block[j+2], block[j+3])) + t = t + 1 + + while t < 64 + s0 = this.smallSigma0(W[t - 15]) + s1 = this.smallSigma1(W[t - 2]) + wt = this.add32_5(W[t - 16], s0, W[t - 7], s1, 0) + // reuse array position via push (we can index existing after push) + push(W, wt) + t = t + 1 + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + f = state[5] + g = state[6] + h = state[7] + + t = 0 + while t < 64 + T1 = this.add32_5(h, this.bigSigma1(e), this.ch(e, f, g), this.K[t], W[t]) + T2 = this.add32(this.bigSigma0(a), this.maj(a, b, c)) + h = g + g = f + f = e + e = this.add32(d, T1) + d = c + c = b + b = a + a = this.add32(T1, T2) + t = t + 1 + + state[0] = this.add32(state[0], a) + state[1] = this.add32(state[1], b) + state[2] = this.add32(state[2], c) + state[3] = this.add32(state[3], d) + state[4] = this.add32(state[4], e) + state[5] = this.add32(state[5], f) + state[6] = this.add32(state[6], g) + state[7] = this.add32(state[7], h) + return state + + fun sha256_bytes(this, bytes) + data = this.pad_bytes(bytes) + // initial hash value (copy IV) + H = [ this.IV[0], this.IV[1], this.IV[2], this.IV[3], this.IV[4], this.IV[5], this.IV[6], this.IV[7] ] + + off = 0 + N = len(data) + while off < N + block = [] + i = 0 + while i < 64 + push(block, data[off + i]) + i = i + 1 + H = this.process_block(H, block) + off = off + 64 + + // digest: 32 bytes big-endian + out = [] + j = 0 + while j < 8 + v = H[j] + push(out, (v / 16777216) % 256) + push(out, (v / 65536) % 256) + push(out, (v / 256) % 256) + push(out, (v) % 256) + j = j + 1 + return out + + fun sha256_hex(this, hexStr) + bytes = this.from_hex(hexStr) + digest = this.sha256_bytes(bytes) + return this.bytes_to_hex(digest) diff --git a/lib/crypt/sha512.fun b/lib/crypt/sha512.fun new file mode 100644 index 0000000..d9ff9ec --- /dev/null +++ b/lib/crypt/sha512.fun @@ -0,0 +1,419 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +// lib/crypt/sha512.fun +// Pure Fun SHA-512 implementation operating on hex-string input. +// 64-bit words are represented as [hi, lo] (two uint32 parts). +// +// Public API (class): +// s = SHA512() +// s.sha512_hex(hexStr) -> digest hex string (lowercase) +// +// Example: +// print(SHA512().sha512_hex("616263")) // "abc" => +// ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad +// 8: that’s for SHA-256; SHA-512 for "abc" is: +// ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a +// 2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f + +class SHA512() + // -------- hex helpers -------- + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + number hi = this.hex_val(substr(hh, 0, 1)) + number lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + hex = to_string(hex) + arr = [] + number i = 0 + number n = len(hex) + while i + 1 < n + number b = this.byte_from_hex_pair(substr(hex, i, 2)) + push(arr, b) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + hexd = "0123456789abcdef" + number hi = (n / 16) % 16 + number lo = n % 16 + c1 = substr(hexd, hi, 1) + c2 = substr(hexd, lo, 1) + return join([c1, c2], "") + + fun bytes_to_hex(this, arr) + out = [] + number i = 0 + number L = len(arr) + while i < L + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + // -------- 32/64 helpers -------- + fun u32(this, x) + m = 4294967296 + while x < 0 + x = x + m + while x >= m + x = x - m + return x + + // 64-bit as [hi, lo] (each uint32) + fun pack64_be(this, b0,b1,b2,b3,b4,b5,b6,b7) + number hi = this.u32(b0 * 16777216 + b1 * 65536 + b2 * 256 + b3) + number lo = this.u32(b4 * 16777216 + b5 * 65536 + b6 * 256 + b7) + return [hi, lo] + + fun add64(this, a, b) + number alo = this.u32(a[1]) + number ahi = this.u32(a[0]) + number blo = this.u32(b[1]) + number bhi = this.u32(b[0]) + number sum_lo = alo + blo + number carry = sum_lo / 4294967296 + sum_lo = sum_lo % 4294967296 + number sum_hi = (ahi + bhi + carry) % 4294967296 + return [sum_hi, sum_lo] + + fun add64_5(this, a, b, c, d, e) + return this.add64(this.add64(this.add64(this.add64(a, b), c), d), e) + + fun xor64(this, a, b) + return [ bxor(a[0], b[0]), bxor(a[1], b[1]) ] + + fun and64(this, a, b) + return [ band(a[0], b[0]), band(a[1], b[1]) ] + + fun or64(this, a, b) + return [ bor(a[0], b[0]), bor(a[1], b[1]) ] + + fun not64(this, a) + return [ bnot(a[0]), bnot(a[1]) ] + + // logical right shift by n (0..63) + fun shr64(this, a, n) + number s = n % 64 + number hi = a[0] + number lo = a[1] + if (s == 0) + return [hi, lo] + else if (s < 32) + number new_lo = bor( shr(lo, s), shl(hi, 32 - s) ) + number new_hi = shr(hi, s) + return [ new_hi, new_lo ] + else + number k = s - 32 + number new_lo2 = shr(hi, k) + return [ 0, new_lo2 ] + + // rotate right by n (0..63) + fun rotr64(this, a, n) + number s = n % 64 + number hi = a[0] + number lo = a[1] + if (s == 0) + return [hi, lo] + else if (s < 32) + number new_lo = bor( shr(lo, s), shl(hi, 32 - s) ) + number new_hi = bor( shr(hi, s), shl(lo, 32 - s) ) + return [ new_hi, new_lo ] + else + number k = s - 32 + // swap roles + number new_lo2 = bor( shr(hi, k), shl(lo, 32 - k) ) + number new_hi2 = bor( shr(lo, k), shl(hi, 32 - k) ) + return [ new_hi2, new_lo2 ] + + // SHA-512 logical functions + fun Ch(this, x, y, z) + // (x & y) ^ (~x & z) + return this.xor64(this.and64(x, y), this.and64(this.not64(x), z)) + + fun Maj(this, x, y, z) + // (x & y) ^ (x & z) ^ (y & z) + return this.xor64(this.xor64(this.and64(x, y), this.and64(x, z)), this.and64(y, z)) + + fun Sigma0(this, x) + return this.xor64(this.xor64(this.rotr64(x, 28), this.rotr64(x, 34)), this.rotr64(x, 39)) + + fun Sigma1(this, x) + return this.xor64(this.xor64(this.rotr64(x, 14), this.rotr64(x, 18)), this.rotr64(x, 41)) + + fun sigma0(this, x) + return this.xor64(this.xor64(this.rotr64(x, 1), this.rotr64(x, 8)), this.shr64(x, 7)) + + fun sigma1(this, x) + return this.xor64(this.xor64(this.rotr64(x, 19), this.rotr64(x, 61)), this.shr64(x, 6)) + + // -------- constants -------- + IV = [ + [0x6a09e667, 0xf3bcc908], [0xbb67ae85, 0x84caa73b], [0x3c6ef372, 0xfe94f82b], [0xa54ff53a, 0x5f1d36f1], + [0x510e527f, 0xade682d1], [0x9b05688c, 0x2b3e6c1f], [0x1f83d9ab, 0xfb41bd6b], [0x5be0cd19, 0x137e2179] + ] + + K = [ + [0x428a2f98,0xd728ae22],[0x71374491,0x23ef65cd],[0xb5c0fbcf,0xec4d3b2f],[0xe9b5dba5,0x8189dbbc], + [0x3956c25b,0xf348b538],[0x59f111f1,0xb605d019],[0x923f82a4,0xaf194f9b],[0xab1c5ed5,0xda6d8118], + [0xd807aa98,0xa3030242],[0x12835b01,0x45706fbe],[0x243185be,0x4ee4b28c],[0x550c7dc3,0xd5ffb4e2], + [0x72be5d74,0xf27b896f],[0x80deb1fe,0x3b1696b1],[0x9bdc06a7,0x25c71235],[0xc19bf174,0xcf692694], + [0xe49b69c1,0x9ef14ad2],[0xefbe4786,0x384f25e3],[0x0fc19dc6,0x8b8cd5b5],[0x240ca1cc,0x77ac9c65], + [0x2de92c6f,0x592b0275],[0x4a7484aa,0x6ea6e483],[0x5cb0a9dc,0xbd41fbd4],[0x76f988da,0x831153b5], + [0x983e5152,0xee66dfab],[0xa831c66d,0x2db43210],[0xb00327c8,0x98fb213f],[0xbf597fc7,0xbeef0ee4], + [0xc6e00bf3,0x3da88fc2],[0xd5a79147,0x930aa725],[0x06ca6351,0xe003826f],[0x14292967,0x0a0e6e70], + [0x27b70a85,0x46d22ffc],[0x2e1b2138,0x5c26c926],[0x4d2c6dfc,0x5ac42aed],[0x53380d13,0x9d95b3df], + [0x650a7354,0x8baf63de],[0x766a0abb,0x3c77b2a8],[0x81c2c92e,0x47edaee6],[0x92722c85,0x1482353b], + [0xa2bfe8a1,0x4cf10364],[0xa81a664b,0xbc423001],[0xc24b8b70,0xd0f89791],[0xc76c51a3,0x0654be30], + [0xd192e819,0xd6ef5218],[0xd6990624,0x5565a910],[0xf40e3585,0x5771202a],[0x106aa070,0x32bbd1b8], + [0x19a4c116,0xb8d2d0c8],[0x1e376c08,0x5141ab53],[0x2748774c,0xdf8eeb99],[0x34b0bcb5,0xe19b48a8], + [0x391c0cb3,0xc5c95a63],[0x4ed8aa4a,0xe3418acb],[0x5b9cca4f,0x7763e373],[0x682e6ff3,0xd6b2b8a3], + [0x748f82ee,0x5defb2fc],[0x78a5636f,0x43172f60],[0x84c87814,0xa1f0ab72],[0x8cc70208,0x1a6439ec], + [0x90befffa,0x23631e28],[0xa4506ceb,0xde82bde9],[0xbef9a3f7,0xb2c67915],[0xc67178f2,0xe372532b], + [0xca273ece,0xea26619c],[0xd186b8c7,0x21c0c207],[0xeada7dd6,0xcde0eb1e],[0xf57d4f7f,0xee6ed178], + [0x06f067aa,0x72176fba],[0x0a637dc5,0xa2c898a6],[0x113f9804,0xbef90dae],[0x1b710b35,0x131c471b], + [0x28db77f5,0x23047d84],[0x32caab7b,0x40c72493],[0x3c9ebe0a,0x15c9bebc],[0x431d67c4,0x9c100d4c], + [0x4cc5d4be,0xcb3e42b6],[0x597f299c,0xfc657e2a],[0x5fcb6fab,0x3ad6faec],[0x6c44198c,0x4a475817] + ] + + // -------- padding -------- + fun pad_bytes(this, bytes) + number L = len(bytes) + out = [] + // copy bytes + number i = 0 + while i < L + push(out, bytes[i]) + i = i + 1 + // append 0x80 + push(out, 128) + // pad zeros until length % 128 == 112 (i.e., 896 bits) + while (len(out) % 128) != 112 + push(out, 0) + // append 128-bit length (we use upper 64 bits zero, lower 64 = L*8) + // upper 8 bytes zero: + number u = 0 + number j = 0 + while j < 8 + push(out, 0) + j = j + 1 + // lower 8 bytes big-endian (most significant byte first) + number bits = L * 8 + number t = 7 + while t >= 0 + number b = (bits / pow(2, 8 * t)) % 256 + push(out, b) + t = t - 1 + return out + + // -------- core -------- + fun process_block(this, H, block) + // W ring buffer of 16 words ([hi, lo]) + W = [] + number i = 0 + while i < 16 + number j = i * 8 + push(W, this.pack64_be(block[j],block[j+1],block[j+2],block[j+3], + block[j+4],block[j+5],block[j+6],block[j+7])) + i = i + 1 + + a = H[0] + b = H[1] + c = H[2] + d = H[3] + e = H[4] + f = H[5] + g = H[6] + h = H[7] + + number t = 0 + while t < 80 + number idx = t % 16 + if (t >= 16) + w2 = W[(t - 2) % 16] + w7 = W[(t - 7) % 16] + w15 = W[(t - 15) % 16] + w16 = W[(t - 16) % 16] // current slot value + s1 = this.sigma1(w2) + s0 = this.sigma0(w15) + tmp = this.add64(w16, s0) + tmp = this.add64(tmp, w7) + W[idx] = this.add64(tmp, s1) + Wt = W[idx] + + // Precompute hot terms once per round + Se1 = this.Sigma1(e) + ch = this.Ch(e, f, g) + + // T1 = h + Se1 + ch + K[t] + Wt (chained to avoid 5-arg helper) + tmp1 = this.add64(h, Se1) + tmp1 = this.add64(tmp1, ch) + tmp1 = this.add64(tmp1, this.K[t]) + T1 = this.add64(tmp1, Wt) + + // T2 = Sigma0(a) + Maj(a, b, c) + T2 = this.add64(this.Sigma0(a), this.Maj(a, b, c)) + + // State update + h = g + g = f + f = e + e = this.add64(d, T1) + d = c + c = b + b = a + a = this.add64(T1, T2) + t = t + 1 + + H[0] = this.add64(H[0], a) + H[1] = this.add64(H[1], b) + H[2] = this.add64(H[2], c) + H[3] = this.add64(H[3], d) + H[4] = this.add64(H[4], e) + H[5] = this.add64(H[5], f) + H[6] = this.add64(H[6], g) + H[7] = this.add64(H[7], h) + return H + + fun sha512_bytes(this, bytes) + data = this.pad_bytes(bytes) + // init H (deep copy IV) + H = [ this.IV[0], this.IV[1], this.IV[2], this.IV[3], this.IV[4], this.IV[5], this.IV[6], this.IV[7] ] + number off = 0 + number N = len(data) + while off < N + block = [] + number i = 0 + while i < 128 + push(block, data[off + i]) + i = i + 1 + H = this.process_block(H, block) + off = off + 128 + + // output 64 bytes big-endian from H[0..7] + out = [] + number j = 0 + while j < 8 + word = H[j] + number hi = word[0] + number lo = word[1] + // hi + push(out, (hi / 16777216) % 256) + push(out, (hi / 65536) % 256) + push(out, (hi / 256) % 256) + push(out, hi % 256) + // lo + push(out, (lo / 16777216) % 256) + push(out, (lo / 65536) % 256) + push(out, (lo / 256) % 256) + push(out, lo % 256) + j = j + 1 + return out + + fun sha512_hex(this, hexStr) + bytes = this.from_hex(hexStr) + digest = this.sha512_bytes(bytes) + return this.bytes_to_hex(digest) + + // Convert a printable ASCII string (0x20..0x7E) to byte array + fun string_to_bytes_ascii(this, s) + str = to_string(s) + out = [] + number i = 0 + // ASCII printable ranges + P1 = " !\"#$%&'()*+,-./" // 32..47 + P2 = "0123456789" // 48..57 + P3 = ":;<=>?@" // 58..64 + P4 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" // 65..90 + P5 = "[\\]^_`" // 91..96 + P6 = "abcdefghijklmnopqrstuvwxyz" // 97..122 + P7 = "{|}~" // 123..126 + while true + ch = substr(str, i, 1) + if (typeof(ch) != "String" || ch == "") + break + number code = -1 + idx = find(P1, ch) + if (idx >= 0) + code = 32 + idx + else + idx = find(P2, ch) + if (idx >= 0) + code = 48 + idx + else + idx = find(P3, ch) + if (idx >= 0) + code = 58 + idx + else + idx = find(P4, ch) + if (idx >= 0) + code = 65 + idx + else + idx = find(P5, ch) + if (idx >= 0) + code = 91 + idx + else + idx = find(P6, ch) + if (idx >= 0) + code = 97 + idx + else + idx = find(P7, ch) + if (idx >= 0) + code = 123 + idx + else + // non-printable -> 0 + code = 0 + push(out, code) + i = i + 1 + return out + + // Hash raw string bytes (printable ASCII). Example: sha512_str("616263") -> ee2109... + fun sha512_str(this, str) + bytes = this.string_to_bytes_ascii(str) + digest = this.sha512_bytes(bytes) + return this.bytes_to_hex(digest) diff --git a/src/bytecode.h b/src/bytecode.h index 03af038..3c51050 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -112,7 +112,17 @@ typedef enum { OP_WRITE_FILE, // pops data string, path string; pushes 1/0 // OS - OP_ENV // pops name string; pushes value string (or "") + OP_ENV, // pops name string; pushes value string (or "") + + // Bitwise (32-bit) and shifts/rotates + OP_BAND, // pops b, a; pushes (uint32_t)(a & b) + OP_BOR, // pops b, a; pushes (uint32_t)(a | b) + OP_BXOR, // pops b, a; pushes (uint32_t)(a ^ b) + OP_BNOT, // pops a; pushes (uint32_t)(~a) + OP_SHL, // pops s, a; pushes (uint32_t)(a << (s&31)) + OP_SHR, // pops s, a; pushes (uint32_t)(a >> (s&31)) logical + OP_ROTL, // pops s, a; pushes rotl32(a, s) + OP_ROTR // pops s, a; pushes rotr32(a, s) } OpCode; typedef struct { diff --git a/src/parser.c b/src/parser.c index 36e5c75..2ff6975 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1073,6 +1073,72 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) return 1; } + /* bitwise ops (32-bit) */ + if (strcmp(name, "band") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "band expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "band expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_BAND, 0); + free(name); + return 1; + } + if (strcmp(name, "bor") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "bor expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "bor expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_BOR, 0); + free(name); + return 1; + } + if (strcmp(name, "bxor") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "bxor expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "bxor expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_BXOR, 0); + free(name); + return 1; + } + if (strcmp(name, "bnot") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "bnot expects 1 arg"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "bnot expects 1 arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_BNOT, 0); + free(name); + return 1; + } + if (strcmp(name, "shl") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "shl expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "shl expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_SHL, 0); + free(name); + return 1; + } + if (strcmp(name, "shr") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "shr expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "shr expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_SHR, 0); + free(name); + return 1; + } + if (strcmp(name, "rol") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "rol expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "rol expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_ROTL, 0); + free(name); + return 1; + } + if (strcmp(name, "ror") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "ror expects 2 args"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "ror expects 2 args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_ROTR, 0); + free(name); + return 1; + } + /* push function value first */ if (local_idx >= 0) { bytecode_add_instruction(bc, OP_LOAD_LOCAL, local_idx); @@ -3308,7 +3374,48 @@ Bytecode *parse_file_to_bytecode(const char *path) { calc_line_col(compile_src, compile_len, g_err_pos, &line, &col); g_err_line = line; g_err_col = col; - fprintf(stderr, "Parse error %s:%d:%d: %s\n", path ? path : "", line, col, g_err_msg); + + /* Try to locate include context marker preceding error */ + const char *marker = "// __include_begin__: "; + size_t mlen = strlen(marker); + int inner_line = -1; + char inc_path[512]; inc_path[0] = '\0'; + /* scan backward to find last marker line */ + size_t scan = g_err_pos; + while (scan > 0) { + /* find start of current line */ + size_t ls = scan; + while (ls > 0 && compile_src[ls - 1] != '\n') ls--; + /* check if this line starts with marker */ + if (ls + mlen <= compile_len && strncmp(compile_src + ls, marker, mlen) == 0) { + /* extract path until end-of-line */ + size_t p = ls + mlen; + size_t pe = p; + while (pe < compile_len && compile_src[pe] != '\n' && (pe - p) < sizeof(inc_path) - 1) pe++; + memcpy(inc_path, compile_src + p, pe - p); + inc_path[pe - p] = '\0'; + /* compute inner line as number of newlines from (pe+1) to error position */ + int count = 1; + size_t q = (pe < compile_len && compile_src[pe] == '\n') ? (pe + 1) : pe; + while (q < g_err_pos) { + if (compile_src[q] == '\n') count++; + q++; + } + inner_line = count; + break; + } + /* move to previous line */ + if (ls == 0) break; + scan = ls - 1; + } + + if (inner_line > 0 && inc_path[0] != '\0') { + fprintf(stderr, "Parse error %s:%d:%d: %s (in %s:%d)\n", + path ? path : "", line, col, g_err_msg, inc_path, inner_line); + } else { + fprintf(stderr, "Parse error %s:%d:%d: %s\n", path ? path : "", line, col, g_err_msg); + } + if (bc) bytecode_free(bc); if (prep) free(prep); free(src); diff --git a/src/parser_utils.c b/src/parser_utils.c index 31c966a..2b01fec 100644 --- a/src/parser_utils.c +++ b/src/parser_utils.c @@ -325,6 +325,10 @@ static char *preprocess_includes_internal(const char *src, int depth) { char *exp = preprocess_includes_internal(inc, depth + 1); free(inc); if (exp) { + /* mark file origin for better error messages */ + sb_append(&out, "// __include_begin__: "); + sb_append(&out, resolved); + sb_append(&out, "\n"); sb_append(&out, exp); /* ensure included chunk ends with newline to preserve line structure */ if (out.len == 0 || out.buf[out.len - 1] != '\n') sb_append_ch(&out, '\n'); diff --git a/src/vm.c b/src/vm.c index f3a4a0d..c20d58d 100644 --- a/src/vm.c +++ b/src/vm.c @@ -25,11 +25,21 @@ static VM *g_active_vm = NULL; static int fun_vm_vfprintf(FILE *stream, const char *fmt, va_list ap) { int written = vfprintf(stream, fmt, ap); if (stream == stderr && g_active_vm && g_active_vm->current_line > 0) { - /* If original message didn't end with newline, add a space first */ - if (written > 0) { - /* best-effort: do not attempt to inspect fmt string fully; just append line info */ + /* Append more context: line number, opcode name, and ip */ + const char *opname = "unknown"; + int ip = -1; + if (g_active_vm->fp >= 0) { + Frame *f = &g_active_vm->frames[g_active_vm->fp]; + ip = f->ip - 1; + if (f->fn && ip >= 0 && ip < f->fn->instr_count) { + int op = f->fn->instructions[ip].op; + if (op >= 0 && op < (int)(sizeof(opcode_names)/sizeof(opcode_names[0]))) { + opname = opcode_names[op]; + } + } } - fprintf(stream == stderr ? stderr : stream, " (line %d)\n", g_active_vm->current_line); + fprintf(stream == stderr ? stderr : stream, " (line %d, op %s @ip %d)\n", + g_active_vm->current_line, opname, ip); } return written; } @@ -249,6 +259,16 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/arrays/slice.c" #include "vm/arrays/zip.c" + /* Bitwise and shifts/rotates */ + #include "vm/bitwise/band.c" + #include "vm/bitwise/bor.c" + #include "vm/bitwise/bxor.c" + #include "vm/bitwise/bnot.c" + #include "vm/bitwise/shl.c" + #include "vm/bitwise/shr.c" + #include "vm/bitwise/rol.c" + #include "vm/bitwise/ror.c" + #include "vm/core/call.c" #include "vm/core/dup.c" #include "vm/core/halt.c" diff --git a/src/vm.h b/src/vm.h index 496fc53..9de96da 100644 --- a/src/vm.h +++ b/src/vm.h @@ -33,7 +33,8 @@ static const char *opcode_names[] = { "ENUMERATE","ZIP", "MIN","MAX","CLAMP","ABS","POW","RANDOM_SEED","RANDOM_INT", "MAKE_MAP","KEYS","VALUES","HAS_KEY", - "READ_FILE","WRITE_FILE","ENV" + "READ_FILE","WRITE_FILE","ENV", + "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR" }; typedef struct { @@ -76,7 +77,7 @@ void vm_dump_globals(VM *vm); void vm_run(VM *vm, Bytecode *entry); static inline int opcode_is_valid(int op) { - return op >= OP_NOP && op <= OP_ENV; // all current opcodes + return op >= OP_NOP && op <= OP_ROTR; // all current opcodes } #endif diff --git a/src/vm/arrays/index_get.c b/src/vm/arrays/index_get.c index 0f08081..40c93f7 100644 --- a/src/vm/arrays/index_get.c +++ b/src/vm/arrays/index_get.c @@ -59,7 +59,8 @@ case OP_INDEX_GET: { free_value(idx); push_value(vm, out); } else { - fprintf(stderr, "Runtime type error: INDEX_GET expects array or map\n"); + fprintf(stderr, "Runtime type error: INDEX_GET expects array or map (got container=%s, index=%s)\n", + value_type_name(container.type), value_type_name(idx.type)); exit(1); } break; diff --git a/src/vm/bitwise/band.c b/src/vm/bitwise/band.c new file mode 100644 index 0000000..7c58e97 --- /dev/null +++ b/src/vm/bitwise/band.c @@ -0,0 +1,27 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_BAND: bitwise AND (uint32 32-bit) + * pops: b, a + * pushes: (uint32_t)(a & b) + */ +case OP_BAND: { + Value vb = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t b = (vb.type == VAL_INT) ? (uint32_t)vb.i : 0u; + uint32_t r = a & b; + free_value(vb); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/bnot.c b/src/vm/bitwise/bnot.c new file mode 100644 index 0000000..c207b9a --- /dev/null +++ b/src/vm/bitwise/bnot.c @@ -0,0 +1,24 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_BNOT: bitwise NOT (uint32 32-bit) + * pops: a + * pushes: (uint32_t)(~a) + */ +case OP_BNOT: { + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t r = ~a; + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/bor.c b/src/vm/bitwise/bor.c new file mode 100644 index 0000000..a831472 --- /dev/null +++ b/src/vm/bitwise/bor.c @@ -0,0 +1,27 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_BOR: bitwise OR (uint32 32-bit) + * pops: b, a + * pushes: (uint32_t)(a | b) + */ +case OP_BOR: { + Value vb = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t b = (vb.type == VAL_INT) ? (uint32_t)vb.i : 0u; + uint32_t r = a | b; + free_value(vb); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/bxor.c b/src/vm/bitwise/bxor.c new file mode 100644 index 0000000..21fa7ec --- /dev/null +++ b/src/vm/bitwise/bxor.c @@ -0,0 +1,27 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_BXOR: bitwise XOR (uint32 32-bit) + * pops: b, a + * pushes: (uint32_t)(a ^ b) + */ +case OP_BXOR: { + Value vb = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t b = (vb.type == VAL_INT) ? (uint32_t)vb.i : 0u; + uint32_t r = a ^ b; + free_value(vb); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/rol.c b/src/vm/bitwise/rol.c new file mode 100644 index 0000000..9d7ae0f --- /dev/null +++ b/src/vm/bitwise/rol.c @@ -0,0 +1,28 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_ROTL: rotate left (uint32) + * pops: s, a + * pushes: rotl32(a, s) + */ +case OP_ROTL: { + Value vs = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t s = (vs.type == VAL_INT) ? (uint32_t)vs.i : 0u; + s &= 31u; + uint32_t r = (s == 0u) ? a : ((a << s) | (a >> (32u - s))); + free_value(vs); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/ror.c b/src/vm/bitwise/ror.c new file mode 100644 index 0000000..fb7f94a --- /dev/null +++ b/src/vm/bitwise/ror.c @@ -0,0 +1,28 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_ROTR: rotate right (uint32) + * pops: s, a + * pushes: rotr32(a, s) + */ +case OP_ROTR: { + Value vs = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t s = (vs.type == VAL_INT) ? (uint32_t)vs.i : 0u; + s &= 31u; + uint32_t r = (s == 0u) ? a : ((a >> s) | (a << (32u - s))); + free_value(vs); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/shl.c b/src/vm/bitwise/shl.c new file mode 100644 index 0000000..e1831a4 --- /dev/null +++ b/src/vm/bitwise/shl.c @@ -0,0 +1,28 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_SHL: logical left shift (uint32) + * pops: s, a + * pushes: (uint32_t)(a << (s&31)) + */ +case OP_SHL: { + Value vs = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t s = (vs.type == VAL_INT) ? (uint32_t)vs.i : 0u; + s &= 31u; + uint32_t r = (s == 0u) ? a : (a << s); + free_value(vs); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/bitwise/shr.c b/src/vm/bitwise/shr.c new file mode 100644 index 0000000..f581ac1 --- /dev/null +++ b/src/vm/bitwise/shr.c @@ -0,0 +1,28 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + * + * Added: 2025-09-29 + */ + +/** + * OP_SHR: logical right shift (uint32) + * pops: s, a + * pushes: (uint32_t)(a >> (s&31)) using logical shift + */ +case OP_SHR: { + Value vs = pop_value(vm); + Value va = pop_value(vm); + uint32_t a = (va.type == VAL_INT) ? (uint32_t)va.i : 0u; + uint32_t s = (vs.type == VAL_INT) ? (uint32_t)vs.i : 0u; + s &= 31u; + uint32_t r = (s == 0u) ? a : (a >> s); + free_value(vs); + free_value(va); + push_value(vm, make_int((int64_t)(uint64_t)r)); + break; +} diff --git a/src/vm/len.c b/src/vm/len.c index 70984c0..8c36078 100644 --- a/src/vm/len.c +++ b/src/vm/len.c @@ -40,8 +40,8 @@ case OP_LEN: { len = array_length(&a); if (len < 0) len = 0; } else { - fprintf(stderr, "Runtime type error: LEN expects array or string\n"); - exit(1); + /* Be lenient: for non-array/non-string, treat length as 0 */ + push_value(vm, make_int(0)); } free_value(a); push_value(vm, make_int(len));