diff --git a/lib/scheme/eval.sx b/lib/scheme/eval.sx index 48a1d208..5af0c24d 100644 --- a/lib/scheme/eval.sx +++ b/lib/scheme/eval.sx @@ -568,6 +568,139 @@ :rules (rest args) :env env})))) +;; ── define-library + import (R7RS Phase 8) ───────────────────── +;; +;; A library is a tagged value with exports + an env where the body +;; was evaluated. The global registry maps a string key (joined from +;; the library-name list) to the library value. +;; +;; (define-library NAME EXPR...) where EXPR can be: +;; (export NAME ...) +;; (import LIB-NAME ...) +;; (begin BODY ...) +;; cond-expand, include, include-library-declarations: deferred. +;; +;; (import LIB-NAME ...) at the top level: for each named library, +;; look up its exports and bind them in the current env. + +(define scheme-library-registry {}) + +(define scm-lib-key + (fn (name) + (cond + ((string? name) name) + ((list? name) (scm-join-strings name "/")) + (:else (error "library name must be symbol or list"))))) + +(define scm-join-strings + (fn (xs sep) + (cond + ((or (nil? xs) (= (length xs) 0)) "") + ((= (length xs) 1) (first xs)) + (:else + (str (first xs) sep (scm-join-strings (rest xs) sep)))))) + +(define scm-library? + (fn (v) + (and (dict? v) (= (get v :scm-tag) :library)))) + +(define scm-collect-exports + (fn (forms acc) + (cond + ((or (nil? forms) (= (length forms) 0)) acc) + (:else + (let ((form (first forms))) + (cond + ((and (list? form) (>= (length form) 1) + (string? (first form)) (= (first form) "export")) + (scm-collect-exports (rest forms) + (scm-list-concat acc (rest form)))) + (:else (scm-collect-exports (rest forms) acc)))))))) + +(define scm-run-library-body + (fn (forms env) + (cond + ((or (nil? forms) (= (length forms) 0)) nil) + (:else + (let ((form (first forms))) + (cond + ;; export/import declarations: handled separately + ((and (list? form) (>= (length form) 1) + (string? (first form)) + (or (= (first form) "export") + (= (first form) "import"))) + (cond + ((= (first form) "import") + (begin + (scm-do-import (rest form) env) + (scm-run-library-body (rest forms) env))) + (:else (scm-run-library-body (rest forms) env)))) + ;; begin: evaluate body + ((and (list? form) (>= (length form) 1) + (string? (first form)) (= (first form) "begin")) + (begin + (scheme-eval-body (rest form) env) + (scm-run-library-body (rest forms) env))) + (:else (scm-run-library-body (rest forms) env)))))))) + +(define scm-do-import + (fn (lib-names env) + (cond + ((or (nil? lib-names) (= (length lib-names) 0)) nil) + (:else + (let ((key (scm-lib-key (first lib-names)))) + (cond + ((not (dict-has? scheme-library-registry key)) + (error (str "import: unknown library: " key))) + (:else + (begin + (let ((lib (get scheme-library-registry key))) + (scm-copy-exports! env + (get lib :exports) + (get lib :env))) + (scm-do-import (rest lib-names) env))))))))) + +(define scm-copy-exports! + (fn (target-env exports source-env) + (cond + ((or (nil? exports) (= (length exports) 0)) nil) + (:else + (let ((name (first exports))) + (cond + ((refl-env-has? source-env name) + (begin + (scheme-env-bind! target-env name + (refl-env-lookup source-env name)) + (scm-copy-exports! target-env (rest exports) source-env))) + (:else + (error (str "import: export not defined: " name))))))))) + +(scheme-define-op! "define-library" + (fn (args env) + (cond + ((< (length args) 1) + (error "define-library: expects (define-library NAME body...)")) + (:else + (let ((lib-name (first args)) + (body (rest args))) + (let ((lib-env (scheme-standard-env)) + (exports (scm-collect-exports body (list))) + (key (scm-lib-key lib-name))) + (begin + (scm-run-library-body body lib-env) + (dict-set! scheme-library-registry key + {:scm-tag :library + :name lib-name + :exports exports + :env lib-env}) + key))))))) + +(scheme-define-op! "import" + (fn (args env) + (begin + (scm-do-import args env) + nil))) + ;; ── define-record-type (R7RS Phase 9) ────────────────────────── ;; ;; (define-record-type NAME diff --git a/lib/scheme/tests/modules.sx b/lib/scheme/tests/modules.sx new file mode 100644 index 00000000..4dc5d841 --- /dev/null +++ b/lib/scheme/tests/modules.sx @@ -0,0 +1,73 @@ +;; lib/scheme/tests/modules.sx — define-library + import. + +(define scm-mod-pass 0) +(define scm-mod-fail 0) +(define scm-mod-fails (list)) + +(define + scm-mod-test + (fn + (name actual expected) + (if + (= actual expected) + (set! scm-mod-pass (+ scm-mod-pass 1)) + (begin + (set! scm-mod-fail (+ scm-mod-fail 1)) + (append! scm-mod-fails {:name name :actual actual :expected expected}))))) + +(define + scm-mod + (fn + (src) + (scheme-eval-program (scheme-parse-all src) (scheme-standard-env)))) + +;; ── Basic define-library + import ─────────────────────────────── + +(scm-mod-test + "simple lib: sq exported" + (scm-mod + "(define-library (my math)\n (export sq)\n (begin (define (sq x) (* x x))))\n (import (my math))\n (sq 5)") + 25) +(scm-mod-test + "lib: multiple exports" + (scm-mod + "(define-library (my math)\n (export sq cube)\n (begin\n (define (sq x) (* x x))\n (define (cube x) (* x x x))))\n (import (my math))\n (list (sq 5) (cube 3))") + (list 25 27)) +(scm-mod-test + "lib: single-symbol name" + (scm-mod + "(define-library (utils)\n (export greet)\n (begin (define (greet name) (string-append \"hi \" name))))\n (import (utils))\n (string=? (greet \"world\") \"hi world\")") + true) + +;; ── Unexported names are not visible ─────────────────────────── + +(scm-mod-test + "lib: private name not exported" + (scm-mod + "(define-library (my math)\n (export sq)\n (begin\n (define (sq x) (* x x))\n (define (private-helper x) (+ x 1))))\n (import (my math))\n (guard (e (else 'unbound)) private-helper)") + "unbound") + +;; ── Library calls its own internals ───────────────────────────── + +(scm-mod-test + "lib: internal calls private fn" + (scm-mod + "(define-library (my math)\n (export public-add1)\n (begin\n (define (private-inc x) (+ x 1))\n (define (public-add1 x) (private-inc x))))\n (import (my math))\n (public-add1 41)") + 42) + +;; ── Two libs, both imported ──────────────────────────────────── + +(scm-mod-test + "two libs: both imported" + (scm-mod + "(define-library (a) (export af) (begin (define (af) 1)))\n (define-library (b) (export bf) (begin (define (bf) 2)))\n (import (a) (b))\n (+ (af) (bf))") + 3) + +;; ── Unknown library import errors ────────────────────────────── + +(scm-mod-test + "import: unknown lib errors" + (scm-mod "(guard (e (else 'unknown-lib)) (import (no such lib)))") + "unknown-lib") + +(define scm-mod-tests-run! (fn () {:total (+ scm-mod-pass scm-mod-fail) :passed scm-mod-pass :failed scm-mod-fail :fails scm-mod-fails}))