Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions pkg/rt/core/hash.lg
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
;; hash — non-cryptographic hash utilities.
;; hash — hash utilities, non-cryptographic and cryptographic.
;;
;; Currently exposes 64-bit xxh3 (https://github.com/zeebo/xxh3) via the
;; raw `xxh3` namespace registered by lginterop. The user-facing API
;; lives here so the friendly names (`xxh3-64`, `xxh3-hasher`) and the
;; stateful-hasher record shape are written in let-go, not Go.
;; Exposes 64-bit xxh3 (https://github.com/zeebo/xxh3) via the raw `xxh3`
;; namespace registered by lginterop. The user-facing API lives here so
;; the friendly names (`xxh3-64`, `xxh3-hasher`) and the stateful-hasher
;; record shape are written in let-go, not Go.
;;
;; The cryptographic digests `sha1` and `sha256` are defined in Go
;; (pkg/rt/hash_sha.go) and seeded into this namespace before this file
;; loads — `(hash/sha1 s)` / `(hash/sha256 s)` return the lowercase hex
;; digest of the string's bytes, matching `sha1sum` / `sha256sum`.
(ns hash)

;; One-shot hashes — keyword bytes or a string in, uint64 out.
Expand Down
Binary file modified pkg/rt/core_compiled.lgb
Binary file not shown.
2 changes: 1 addition & 1 deletion pkg/rt/generated.sums
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Content digest of all .lg + lgbgen sources that feed the .lgb
# bundle and the lowered Go tree. The genmanifest staleness test
# fails if this no longer matches the sources on disk.
09573140e5fca8b882590526f778b496cd2acdad46d23a5ae225e4764989ef9b
0d12d62ce4179aafd6ab53cced499234576c4356b6106e1ac8ba79bc43a50b3a
67 changes: 67 additions & 0 deletions pkg/rt/hash_sha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2026 let-go contributors
* SPDX-License-Identifier: MIT
*
* SHA-1 and SHA-256 digests as `hash/sha1` and `hash/sha256`.
*
* No build tags: crypto/sha1 and crypto/sha256 are pure Go, so tinygo/wasm
* builds get these too (unlike xxh3, which needs the murmur3 fallback).
*
* The installer seeds the `hash` namespace from Go; pkg/rt/core/hash.lg
* later declares `(ns hash)` and EXTENDS this namespace rather than
* replacing it (LookupOrRegisterNS returns the registered instance).
*
* let-go strings are raw Go byte strings, so these digest the input's bytes
* — a jar slurped from disk hashes to the same hex `sha1sum` prints.
*/

package rt

import (
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"

"github.com/nooga/let-go/pkg/vm"
)

func init() { RegisterInstaller(installHashShaNS) }

// shaArg validates the single String argument shared by both digests.
func shaArg(name string, vs []vm.Value) (string, error) {
if len(vs) != 1 {
return "", fmt.Errorf("%s expects 1 arg", name)
}
s, ok := vs[0].(vm.String)
if !ok {
return "", fmt.Errorf("%s expected String", name)
}
return string(s), nil
}

func installHashShaNS() {
ns := vm.NewNamespace("hash")

// hash/sha1 — (hash/sha1 s) → lowercase hex SHA-1 of s's bytes.
ns.Def("sha1", mustWrap(func(vs []vm.Value) (vm.Value, error) {
s, err := shaArg("hash/sha1", vs)
if err != nil {
return vm.NIL, err
}
sum := sha1.Sum([]byte(s))
return vm.String(hex.EncodeToString(sum[:])), nil
}))

// hash/sha256 — (hash/sha256 s) → lowercase hex SHA-256 of s's bytes.
ns.Def("sha256", mustWrap(func(vs []vm.Value) (vm.Value, error) {
s, err := shaArg("hash/sha256", vs)
if err != nil {
return vm.NIL, err
}
sum := sha256.Sum256([]byte(s))
return vm.String(hex.EncodeToString(sum[:])), nil
}))

RegisterNS(ns)
}
45 changes: 45 additions & 0 deletions test/hash_sha_test.lg
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
;; hash/sha1 and hash/sha256 — lowercase hex digests of a string's bytes.
(ns test.hash-sha-test
(:require [test :refer :all]))

(deftest sha1-known-vectors
(testing "empty string"
(is (= "da39a3ee5e6b4b0d3255bfef95601890afd80709" (hash/sha1 ""))))
(testing "abc"
(is (= "a9993e364706816aba3e25717850c26c9cd0d89d" (hash/sha1 "abc")))))

(deftest sha256-known-vectors
(testing "empty string"
(is (= "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
(hash/sha256 ""))))
(testing "abc"
(is (= "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
(hash/sha256 "abc")))))

(deftest sha-hashes-bytes-not-runes
;; A multibyte literal pins byte semantics: these are the digests
;; `sha1sum`/`sha256sum` produce for the UTF-8 encoding (17 bytes, 13 runes).
(testing "multibyte string matches the shasum of its UTF-8 bytes"
(is (= "a5e7f35caea50aa6f3bc37d2f24a540fc0b3cb32" (hash/sha1 "héllo wörld ✓")))
(is (= "c2a59c71097b678dc5af2eb1f98ddc575b63948b0fa6740071a945673aaada4d"
(hash/sha256 "héllo wörld ✓")))))

(deftest sha-spit-slurp-roundtrip
(testing "digest survives a write/read round-trip through the filesystem"
(let [path (str (os/temp-dir) "/let-go-hash-sha-test.bin")
content "héllo wörld ✓"]
(spit path content)
(let [back (slurp path)]
(is (= content back))
(is (= (hash/sha1 content) (hash/sha1 back)))
(is (= (hash/sha256 content) (hash/sha256 back))))
(delete-file path))))

(deftest sha-rejects-bad-args
;; let-go has no thrown? — use try/catch directly.
(testing "non-string arg throws"
(is (= :threw (try (hash/sha1 42) :no-throw (catch _ :threw))))
(is (= :threw (try (hash/sha256 42) :no-throw (catch _ :threw)))))
(testing "wrong arity throws"
(is (= :threw (try (hash/sha1) :no-throw (catch _ :threw))))
(is (= :threw (try (hash/sha256 "a" "b") :no-throw (catch _ :threw))))))
Loading