diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e7d882..b7432e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.16.0 LANGUAGES C) +project(fun VERSION 0.16.1 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/sha1_demo.fun b/examples/sha1_demo.fun new file mode 100644 index 0000000..4cd14ec --- /dev/null +++ b/examples/sha1_demo.fun @@ -0,0 +1,48 @@ +#!/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 Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-01 + */ + +/* + * SHA-1 usage demo + * + * Run without installing: + * FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/sha1_demo.fun + */ + +#include as sha1 + +print("=== SHA-1 demo ===") + +s = "abc" +hex_abc = "616263" // "abc" in hex +empty = "" + +// Hash ASCII string directly +d1 = sha1.SHA1().sha1_str(s) +print("SHA-1('abc') = " + d1) + +// Hash pre-encoded hex bytes +d2 = sha1.SHA1().sha1_hex(hex_abc) +print("SHA-1(616263 hex) = " + d2) + +// Empty string +d3 = sha1.SHA1().sha1_str(empty) +print("SHA-1('') = " + d3) + +print("=== done ===") + +/* Expected output: +=== SHA-1 demo === +SHA-1('abc') = e6cd2bcee460c45d41565ac877d866a159a16e19 +SHA-1(616263 hex) = e6cd2bcee460c45d41565ac877d866a159a16e19 +SHA-1('') = da39a3ee5e6b4b0d3255bfef95601890afd80709 +=== done === diff --git a/lib/crypt/crc32.fun b/lib/crypt/crc32.fun deleted file mode 100644 index 5f1c8ee..0000000 --- a/lib/crypt/crc32.fun +++ /dev/null @@ -1,187 +0,0 @@ -/* - * This file is part of the Fun programming language. - * https://hanez.org/project/fun/ - * - * Copyright 2025 Johannes Findeisen - * Licensed under the terms of the Apache-2.0 license. - * https://opensource.org/license/apache-2-0 - * - * 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/sha1.fun b/lib/crypt/sha1.fun new file mode 100644 index 0000000..1e41d0b --- /dev/null +++ b/lib/crypt/sha1.fun @@ -0,0 +1,265 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-10-01 + */ + +// lib/crypt/sha1.fun +// Pure Fun SHA-1 implementation operating on hex-string or ASCII inputs. +// +// Public API (class): +// sha = SHA1() +// sha.sha1_hex(hexStr) -> digest hex string (lowercase) +// sha.sha1_str("abc") -> digest hex string of ASCII bytes + +class SHA1() + // 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 rol32(this, x, s) + x = this.u32(x) + return rol(x, s) + + // 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-1): append 0x80, then zeros until length ≡ 56 (mod 64), then 64-bit length big-endian + 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 = 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) + return this.u32(b0 * 16777216 + b1 * 65536 + b2 * 256 + b3) + + // process 512-bit block + fun process_block(this, state, block) + // W[80] + 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 < 80 + wt = bxor(bxor(bxor(W[t-3], W[t-8]), W[t-14]), W[t-16]) + wt = this.rol32(wt, 1) + push(W, wt) + t = t + 1 + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + t = 0 + while t < 80 + if (t < 20) + f = bor(band(b, c), band(bnot(b), d)) + k = 1518500249 // 0x5A827999 + else if (t < 40) + f = bxor(b, bxor(c, d)) + k = 1859775393 // 0x6ED9EBA1 + else if (t < 60) + f = bor(bor(band(b, c), band(b, d)), band(c, d)) + k = 2400959708 // 0x8F1BBCDC + else + f = bxor(b, bxor(c, d)) + k = 3395469782 // 0xCA62C1D6 + + temp = this.u32(this.u32(this.rol32(a, 5) + f) + this.u32(this.u32(e + k) + W[t])) + e = d + d = c + c = this.rol32(b, 30) + b = a + a = temp + 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) + return state + + fun sha1_bytes(this, bytes) + // initial H + H = [ 1732584193, 4023233417, 2562383102, 271733878, 3285377520 ] + data = this.pad_bytes(bytes) + N = len(data) + off = 0 + 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 + + // output 20-byte big-endian + out = [] + j = 0 + while j < 5 + 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 sha1_hex(this, hexStr) + bytes = this.from_hex(hexStr) + digest = this.sha1_bytes(bytes) + return this.bytes_to_hex(digest) + + // ASCII string to bytes (printable ASCII 0x20..0x7E) + 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 + + fun sha1_str(this, str) + bytes = this.string_to_bytes_ascii(str) + digest = this.sha1_bytes(bytes) + return this.bytes_to_hex(digest) diff --git a/src/parser.c b/src/parser.c index c3235ec..d637b0d 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1135,6 +1135,8 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) int gi = sym_index(name); bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi); } + /* Track namespace alias only for the initial receiver; after any call it's no longer an alias value */ + int __ns_ctx = is_ns_alias(name); /* parse arguments */ (*pos)++; /* '(' */ int argc = 0; @@ -1213,20 +1215,15 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) continue; } - /* If base identifier is a namespace alias -> treat as plain function call: no implicit 'this' */ - int is_ns = is_ns_alias(name); + /* After a function call, the receiver is a value (not a namespace alias); + always treat dot-call as a method with implicit 'this'. */ + int is_ns = 0; - if (!is_ns) { - /* Method sugar with implicit 'this' */ - bytecode_add_instruction(bc, OP_DUP, 0); - bytecode_add_instruction(bc, OP_LOAD_CONST, kci); - bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> stack: obj, func */ - bytecode_add_instruction(bc, OP_SWAP, 0); /* -> stack: func, obj (this) */ - } else { - /* Plain property function call: obj["mname"] -> func */ - bytecode_add_instruction(bc, OP_LOAD_CONST, kci); - bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> stack: func */ - } + /* Method sugar with implicit 'this' */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, kci); + bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> stack: obj, func */ + bytecode_add_instruction(bc, OP_SWAP, 0); /* -> stack: func, obj (this) */ /* Consume '(' and parse args */ *pos = callp + 1; @@ -1265,6 +1262,8 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) int gi = sym_index(name); bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi); } + /* track namespace alias only for the initial receiver; after any call it's no longer an alias value */ + int __ns_ctx = is_ns_alias(name); /* postfix indexing, slice, and dot access/method calls */ for (;;) { skip_spaces(src, len, pos); @@ -1318,7 +1317,8 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) continue; } - int is_ns = is_ns_alias(name); + /* Use initial alias context for only the first dot-call; reset after call */ + int is_ns = __ns_ctx; if (!is_ns) { /* Method sugar with implicit 'this' */ @@ -1345,6 +1345,9 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after arguments"); free(mname); free(name); return 0; } bytecode_add_instruction(bc, OP_CALL, is_ns ? argc : (argc + 1)); + /* After any call, the receiver is now a value, not a namespace alias */ + __ns_ctx = 0; + free(mname); continue; } else {