feat: resolve host-provided namespaces
8c9eb04 parent: e5a8bce modified
src/namespaces.nim +24 -2 | @@ -16,7 +16,7 @@ type | ||
| 16 | 16 | defs, privateDefs, required: HashSet[string] |
| 17 | 17 | Resolver = ref object |
| 18 | 18 | roots: seq[string] |
| 19 | - cores: HashSet[string] | |
| 19 | + cores, hosts: HashSet[string] | |
| 20 | 20 | spaces: Table[string, Namespace] |
| 21 | 21 | active: seq[string] |
| 22 | 22 | loaded: HashSet[string] |
| @@ -64,6 +64,22 @@ proc registeredCoreNames(): HashSet[string] = | ||
| 64 | 64 | finally: |
| 65 | 65 | globals = saved |
| 66 | 66 | |
| 67 | +proc registeredHostNames(): HashSet[string] = | |
| 68 | + ## Runtime functions in named host namespaces (for example clojure.string) | |
| 69 | + ## do not need a shadow source file merely to be required by user code. | |
| 70 | + let saved = globals | |
| 71 | + globals = initTable[string, VarCell]() | |
| 72 | + try: | |
| 73 | + registerCore() | |
| 74 | + for name, cell in globals: | |
| 75 | + if cell.bound: result.incl name | |
| 76 | + finally: | |
| 77 | + globals = saved | |
| 78 | + | |
| 79 | +proc hasHostNamespace(r: Resolver, name: string): bool = | |
| 80 | + for candidate in r.hosts: | |
| 81 | + if candidate.startsWith(name & "/"): return true | |
| 82 | + | |
| 67 | 83 | proc registerNamespaceCore*() = |
| 68 | 84 | ## Call after registerCore in the generated program, before resolving cells. |
| 69 | 85 | ## Both spellings share a cell, so rebinding remains coherent. |
| @@ -85,6 +101,7 @@ proc checkVar(r: Resolver, ns: Namespace, target, name: string, | ||
| 85 | 101 | fail("no definition " & canonical) |
| 86 | 102 | if target != ns.name and target notin ns.required: |
| 87 | 103 | fail("namespace " & target & " is not required by " & ns.name) |
| 104 | + if canonical in r.hosts: return | |
| 88 | 105 | if not r.spaces.hasKey(target) or name notin r.spaces[target].defs: |
| 89 | 106 | fail("no definition " & canonical) |
| 90 | 107 | if target != ns.name and name in r.spaces[target].privateDefs: |
| @@ -299,6 +316,10 @@ proc process(r: Resolver, forms: seq[Value], expected = "") | ||
| 299 | 316 | proc load(r: Resolver, name: string) = |
| 300 | 317 | validNamespace(name) |
| 301 | 318 | if name == "clojure.core": return |
| 319 | + if r.hasHostNamespace(name): | |
| 320 | + discard r.space(name) | |
| 321 | + r.loaded.incl name | |
| 322 | + return | |
| 302 | 323 | if name in r.active: fail("dependency cycle: " & (r.active & @[name]).join(" -> ")) |
| 303 | 324 | if name in r.loaded: return |
| 304 | 325 | let relative = name.replace('.', '/').replace('-', '_') |
| @@ -410,6 +431,7 @@ proc resolveSource*(src: string, sourceRoots: seq[string]): seq[Value] = | ||
| 410 | 431 | ## Forward references require declare. Qualified references require a direct |
| 411 | 432 | ## require, except for the current namespace and implicit clojure.core. |
| 412 | 433 | ## Unknown and externally private vars are rejected during analysis. |
| 413 | - let r = Resolver(roots: sourceRoots, cores: registeredCoreNames()) | |
| 434 | + let r = Resolver(roots: sourceRoots, cores: registeredCoreNames(), | |
| 435 | + hosts: registeredHostNames()) | |
| 414 | 436 | r.process(readAll(src)) |
| 415 | 437 | r.output |
| @@ -16,7 +16,7 @@ type | |||
| 16 | defs, privateDefs, required: HashSet[string] | 16 | defs, privateDefs, required: HashSet[string] |
| 17 | Resolver = ref object | 17 | Resolver = ref object |
| 18 | roots: seq[string] | 18 | roots: seq[string] |
| 19 | - cores: HashSet[string] | 19 | + cores, hosts: HashSet[string] |
| 20 | spaces: Table[string, Namespace] | 20 | spaces: Table[string, Namespace] |
| 21 | active: seq[string] | 21 | active: seq[string] |
| 22 | loaded: HashSet[string] | 22 | loaded: HashSet[string] |
| @@ -64,6 +64,22 @@ proc registeredCoreNames(): HashSet[string] = | |||
| 64 | finally: | 64 | finally: |
| 65 | globals = saved | 65 | globals = saved |
| 66 | 66 | ||
| 67 | +proc registeredHostNames(): HashSet[string] = | ||
| 68 | + ## Runtime functions in named host namespaces (for example clojure.string) | ||
| 69 | + ## do not need a shadow source file merely to be required by user code. | ||
| 70 | + let saved = globals | ||
| 71 | + globals = initTable[string, VarCell]() | ||
| 72 | + try: | ||
| 73 | + registerCore() | ||
| 74 | + for name, cell in globals: | ||
| 75 | + if cell.bound: result.incl name | ||
| 76 | + finally: | ||
| 77 | + globals = saved | ||
| 78 | + | ||
| 79 | +proc hasHostNamespace(r: Resolver, name: string): bool = | ||
| 80 | + for candidate in r.hosts: | ||
| 81 | + if candidate.startsWith(name & "/"): return true | ||
| 82 | + | ||
| 67 | proc registerNamespaceCore*() = | 83 | proc registerNamespaceCore*() = |
| 68 | ## Call after registerCore in the generated program, before resolving cells. | 84 | ## Call after registerCore in the generated program, before resolving cells. |
| 69 | ## Both spellings share a cell, so rebinding remains coherent. | 85 | ## Both spellings share a cell, so rebinding remains coherent. |
| @@ -85,6 +101,7 @@ proc checkVar(r: Resolver, ns: Namespace, target, name: string, | |||
| 85 | fail("no definition " & canonical) | 101 | fail("no definition " & canonical) |
| 86 | if target != ns.name and target notin ns.required: | 102 | if target != ns.name and target notin ns.required: |
| 87 | fail("namespace " & target & " is not required by " & ns.name) | 103 | fail("namespace " & target & " is not required by " & ns.name) |
| 104 | + if canonical in r.hosts: return | ||
| 88 | if not r.spaces.hasKey(target) or name notin r.spaces[target].defs: | 105 | if not r.spaces.hasKey(target) or name notin r.spaces[target].defs: |
| 89 | fail("no definition " & canonical) | 106 | fail("no definition " & canonical) |
| 90 | if target != ns.name and name in r.spaces[target].privateDefs: | 107 | if target != ns.name and name in r.spaces[target].privateDefs: |
| @@ -299,6 +316,10 @@ proc process(r: Resolver, forms: seq[Value], expected = "") | |||
| 299 | proc load(r: Resolver, name: string) = | 316 | proc load(r: Resolver, name: string) = |
| 300 | validNamespace(name) | 317 | validNamespace(name) |
| 301 | if name == "clojure.core": return | 318 | if name == "clojure.core": return |
| 319 | + if r.hasHostNamespace(name): | ||
| 320 | + discard r.space(name) | ||
| 321 | + r.loaded.incl name | ||
| 322 | + return | ||
| 302 | if name in r.active: fail("dependency cycle: " & (r.active & @[name]).join(" -> ")) | 323 | if name in r.active: fail("dependency cycle: " & (r.active & @[name]).join(" -> ")) |
| 303 | if name in r.loaded: return | 324 | if name in r.loaded: return |
| 304 | let relative = name.replace('.', '/').replace('-', '_') | 325 | let relative = name.replace('.', '/').replace('-', '_') |
| @@ -410,6 +431,7 @@ proc resolveSource*(src: string, sourceRoots: seq[string]): seq[Value] = | |||
| 410 | ## Forward references require declare. Qualified references require a direct | 431 | ## Forward references require declare. Qualified references require a direct |
| 411 | ## require, except for the current namespace and implicit clojure.core. | 432 | ## require, except for the current namespace and implicit clojure.core. |
| 412 | ## Unknown and externally private vars are rejected during analysis. | 433 | ## Unknown and externally private vars are rejected during analysis. |
| 413 | - let r = Resolver(roots: sourceRoots, cores: registeredCoreNames()) | 434 | + let r = Resolver(roots: sourceRoots, cores: registeredCoreNames(), |
| 435 | + hosts: registeredHostNames()) | ||
| 414 | r.process(readAll(src)) | 436 | r.process(readAll(src)) |
| 415 | r.output | 437 | r.output |
modified
tests/test_namespaces.nim +4 -0 | @@ -28,6 +28,10 @@ suite "static namespace resolution": | ||
| 28 | 28 | check forms[0].kind == kStr |
| 29 | 29 | check forms[0].s == "(?i)^did:[a-z0-9]+:" |
| 30 | 30 | |
| 31 | + test "host namespaces resolve without placeholder source files": | |
| 32 | + checkForms("(ns user (:require [clojure.string :as str])) (str/trim \" hi \")", | |
| 33 | + "(clojure.string/trim \" hi \")") | |
| 34 | + | |
| 31 | 35 | test "definitions resolve sequentially, including core shadowing": |
| 32 | 36 | checkForms("(inc 1) (def inc (fn [x] x)) (inc 2)", |
| 33 | 37 | "(clojure.core/inc 1) (def user/inc (fn [x] x)) (user/inc 2)") |
| @@ -28,6 +28,10 @@ suite "static namespace resolution": | |||
| 28 | check forms[0].kind == kStr | 28 | check forms[0].kind == kStr |
| 29 | check forms[0].s == "(?i)^did:[a-z0-9]+:" | 29 | check forms[0].s == "(?i)^did:[a-z0-9]+:" |
| 30 | 30 | ||
| 31 | + test "host namespaces resolve without placeholder source files": | ||
| 32 | + checkForms("(ns user (:require [clojure.string :as str])) (str/trim \" hi \")", | ||
| 33 | + "(clojure.string/trim \" hi \")") | ||
| 34 | + | ||
| 31 | test "definitions resolve sequentially, including core shadowing": | 35 | test "definitions resolve sequentially, including core shadowing": |
| 32 | checkForms("(inc 1) (def inc (fn [x] x)) (inc 2)", | 36 | checkForms("(inc 1) (def inc (fn [x] x)) (inc 2)", |
| 33 | "(clojure.core/inc 1) (def user/inc (fn [x] x)) (user/inc 2)") | 37 | "(clojure.core/inc 1) (def user/inc (fn [x] x)) (user/inc 2)") |