Login
7 branches 0 tags
Benjamin Vincent Schulenburg Added support for categorizing λs 5c9625e 3 years ago 902 Commits
nujel / stdlib / core / module.nuj
;;; Nujel - Copyright (C) 2020-2021 - Benjamin Vincent Schulenburg
;;; This project uses the MIT license, a copy should be included under /LICENSE
;;;
;;; Contains the module system

[def module/cache @[]]
[def module/store @[]]
[def module/loader #nil]

[defn module/add-loader [f]
      [set! module/loader [cons f module/loader]]]

[defn module/save-state []
      @[:cache module/cache :loader module/loader]]

[defn module/restore-state [c]
      [set! module/cache [tree/ref c :cache]]
      [set! module/loader [tree/ref c :loader]]]

[defn module/qualify-symbol [module-name symbol]
      [case [type-of module-name]
            [:string [module/qualify-symbol [string->keyword module-name] symbol]]
            [:keyword [string->symbol [cat [keyword->symbol module-name] "/" [string symbol]]]]
            [otherwise [exception "Can't qualify that" module-name]]]]

[defmacro module body
          "Define a new module and return it"
          [macroexpand [cons 'environment* [cons '[def exports @[]] body]]]]

[defmacro defmodule [name . body]
          "Define a new named module"
          `[module/insert ~name [module [def *module* ~name] ~@body]]]

[defmacro defmodule/defer [name . body]
          "Define a new named module"
          `[module/insert/defer ~name [string/write ~[list 'quote [cons 'do body]]]]]

[defmacro export [name value]
          `[tree/set! exports '~name ~value]]

[defn require* [module env qualify?]
      [def mod [module/load module env]]
      [when-not mod [exception "Can't load that module" module]]
      [def exports [resolve 'exports mod]]
      [doseq [k [tree/keys exports]]
             [def-in! env
               [if qualify?
                   [module/qualify-symbol module [keyword->symbol k]]
                   [keyword->symbol k]]
               [tree/ref exports k]]]]

[defmacro use [module]
          `[require* ~module [current-closure] #f]]

[defmacro require [module]
          `[require* ~module [current-closure] #t]]

[defmacro import* [local-symbol module module-symbol]
          `[def ~local-symbol [module/import ~module '~module-symbol]]]

[defmacro import [names module]
          [when-not [list? names]
                    [return `[import* ~names [module/load ~module [current-closure]] ~names]]]
          [def ret #nil]
          [while names
                 [if [= [cadr names] :as]
                     [do [set! ret [cons `[import* ~[caddr names] [module/load ~module [current-closure]] ~[car names]] ret]]
                         [set! names [cddr names]]]
                     [set! ret [cons `[import* ~[car names] [module/load ~module [current-closure]] ~[car names]] ret]]]
                 [cdr! names]]
          [cons do [nreverse ret]]]

[defn module/load/cache [name]
      [tree/ref module/cache name]]

[defn module/load/store [name]
      [def source [tree/ref module/store name]]
      [when source
        [eval-in [environment*] `[defmodule ~name ~@[read source]]]
        [module/load/cache name]]]

[defn module/load/external [name]
      [doseq [loader module/loader]
             [def mod [loader name]]
             [when mod
               [tree/set! module/cache name mod]
               [return mod]]]]

[defn module/insert/defer [name module-source]
      [tree/set! module/store name module-source]]

[defn module/insert [name module]
      [tree/set! module/cache name module]]

[defn module/resolve-string/join [parts ret]
      [cond [[not parts] ret]
            [[or [== "" [car parts]]
                 [== "." [car parts]]]
             [module/resolve-string/join [cdr parts] ret]]
            [[== ".." [car parts]]
             [when-not [cadr parts]
                       [exception "Invalid path" parts]]
             [module/resolve-string/join [cddr parts] ret]]
            [otherwise [module/resolve-string/join [cdr parts] [cons [car parts] ret]]]]]

[defn module/resolve-string [name wd]
      [def mod-path wd]
      [def parts [module/resolve-string/join [nreverse [append [split mod-path "/"] [split name "/"]]] #nil]]
      [string->keyword [join [cons "" parts] "/"]]] ; The cons is for the leading /

[defn module/load [name env]
      [case [type-of name]
            [:object name]
            [:string [module/load [module/resolve-string name [resolve '*module-path* env]] env]]
            [:keyword [or [module/load/cache name]
                          [module/load/store name]
                          [module/load/external name]]]
            [otherwise [exception "Can't load that value as a module" name]]]]

[defn module/import-all [module symbol]
      [def exports [resolve 'exports module]]
      [typecheck/only exports :tree]
      [return exports]]

[defn module/import [module symbol]
      [def exports [module/import-all module symbol]]
      [when-not [tree/has? exports symbol]
                [exception :import-error [if [resolves? symbol module]
                                             "That symbol was not exported"
                                             "That symbol does not exist in that module"]]]
      [tree/ref exports symbol]]

[defn module/main [module args]
      "Import and run MODULE's main function with ARGS"
      [def mod [module/load module [current-closure]]]
      [when-not mod [exception "Module not found" module]]
      [def exports [resolve 'exports mod]]
      [when-not exports [exception "Invalid module, can't resolve exports" module]]
      [def main [tree/ref exports :main]]
      [when-not main [exception "There is no symbol exported as main" module]]
      [when-not [procedure? main] [exception "main is not a callable value" module]]
      [main args]]