sourceafIoc::RegistryBuilder.fan


** Use to create an IoC `Registry`. 
** 
** Add module classes, scopes, services, and contributions.
@Js
class RegistryBuilder {
    private const Log       _logger         := RegistryBuilder#.pod.log

    private OneShotLock     _lock           := OneShotLock(ErrMsgs.registry_alreadyBuilt)
    private ScpDef[]        _scopeDefs      := ScpDef[,]
    internal SrvDef[]       _serviceDefs    := SrvDef[,]
    internal OvrDef[]       _overrideDefs   := OvrDef[,]
    internal ContribDef[]   _contribDefs    := ContribDef[,]
    private Obj[]           _modulesAdd     := Obj[,]
    private Obj[]           _modulesAddAdd  := Obj[,]
    private Obj[]           _modulesRemove  := Obj[,]
    private Pod[]           _pods           := Pod[,]
    private Type[]          _moduleStack    := Type[,]
    
    private Obj[][]         _registryStartupHooks   := Obj[][,]
    private Obj[][]         _registryShutdownHooks  := Obj[][,]
    private Obj[][]         _scopeCreateHooks       := Obj[][,]
    private Obj[][]         _scopeDestroyHooks      := Obj[][,]
    private Obj[][]         _serviceBuildHooks      := Obj[][,]
    private Obj[][]         _decoratorHooks         := Obj[][,]
    private Duration        _buildStart
    
    @NoDoc
    ModuleInspector[]       inspectors

    ** Use options to pass state into the IoC Registry. 
    ** This map may be later retrieved from the `RegistryMeta` service. 
    Str:Obj?    options := Str:Obj?[:] { it.caseInsensitive = true } {
        private set
    }
    
    ** Set to 'true' to suppress builder logging.
    @NoDoc Bool suppressLogging := false

    @NoDoc
    new make() {
        _buildStart = Duration.now
        inspectors  = [
            StandardInspector(),
            FacetInspector(),
            NonInvasiveInspector()
        ]
        addModule(IocModule())
    }

    ** Suppresses surplus logging by 'RegistryBuilder' and 'Registry'.
    ** 
    ** Removes the following contributions from registry startup: 
    **  - 'afIoc.logBanner'
    **  - 'afIoc.logServices'
    ** 
    ** And the following from registry shutdown:
    **  - 'afIoc.sayGoodbye'
    This silent() {
        suppressLogging = true
        onRegistryStartup |Configuration config| {
            config.remove("afIoc.logBanner",        "afIoc.silentBanner")
            config.remove("afIoc.logServices",      "afIoc.silentServices")
            config.remove("afIoc.logStartupTimes",  "afIoc.silentStartupTimes")
        }
        onRegistryShutdown|Configuration config| {
            config.remove("afIoc.sayGoodbye", "afIoc.silentBoodbye")
        }
        return this
    }
    
    ** Adds a module to the registry. The given 'module' may be:
    **  - a module instance, 
    **  - a module 'Type'
    **  - a qualified module name ( 'Str' )
    **  - a pod name ( 'Str' )
    **  - 'null' (does nothing)
    ** 
    ** If a pod name then dependencies are also added.
    ** 
    ** Any modules defined with the '@SubModule' facet are also added.
    ** 
    ** Don't forget you can always call 'removeModule(...)' too!
    This addModule(Obj? module) {
        _lock.check
        
        if (module == null) return this
        
        // check _modulesRemove in case module is added during inspection
        moduleType := ((Type) (module is Type ? module : module.typeof)).toNonNullable
        if (_modulesRemove.contains(moduleType)) {
            _logger.debug(ErrMsgs.regBuilder_ignoringModule(moduleType))
            return this
        }

        if (!moduleType.isConst)
            throw ArgErr(ErrMsgs.regBuilder_modulesShouldBeConst(moduleType))
        
        // Disallow adding the same module type twice - even if different instances 'cos it's too much hassle
        if (_modulesAdd.any { it.typeof == moduleType } || _modulesAddAdd.any { it.typeof == moduleType }) {
            // Debug because sometimes you can't help adding the same module twice (via dependencies)
            // afBedSheet is a prime example
            _logger.debug(ErrMsgs.regBuilder_moduleAlreadyAdded(moduleType))
            return this
        }
        
        if (!suppressLogging && moduleType != IocModule#)
            _logger.info("Adding module ${moduleType.qname}")

        // be nice and check for Strs
        if (module is Str) {
            if (Pod.find(module, false) != null)
                return addModulesFromPod(module, true)
            module = Type.find(module, true)
        }
        
        if (module is Type) 
            module = ((Type) module).make

        _modulesAdd.add(module)
        
        // need to double add 'cos we clear _modulesAdd during inspection 
        _modulesAddAdd.add(module)
        
        return this
    }

    ** Adds many modules to the registry. 
    This addModules(Obj[] modules) {
        modules.each { addModule(it) }
        return this
    }

    ** Removes modules of the given type. If a module of the given type is subsequently added, it is silently ignored.
    This removeModule(Type? moduleType) {
        _lock.check

        if (moduleType == null) return this

        if (moduleType.fits(IocModule#))
            throw ArgErr(ErrMsgs.regBuilder_cannotRemoveModule(IocModule#))

        if (!suppressLogging)
            _logger.info("Removing module ${moduleType.qname}")

        // remove all now
        _modulesAdd = _modulesAdd.exclude { it.typeof == moduleType }

        // prevent it from being added later
        _modulesRemove.add(moduleType.toNonNullable)
        return this
    }

    ** Inspects the [pod's meta-data]`docLang::Pods#meta` for the key 'afIoc.module'. This is then 
    ** treated as a CSV list of (qualified) module type names to load.
    ** 
    ** If 'addDependencies' is 'true' then the pod's dependencies are also inspected for IoC 
    ** modules. 
    This addModulesFromPod(Str podName, Bool addDependencies := true) {
        _lock.check
        pod := Pod.find(podName)
        if (!suppressLogging)
            _logger.info("Adding module definitions from pod '$pod.name'")
        _addModulesFromPod(pod, addDependencies)
        return this
    }

    ** Defines a new scope to be used with the registry.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.addScope("thread")
    ** <pre
    ScopeBuilder addScope(Str scopeId, Bool threaded := true) {
        _lock.check
        scopeDef := ScpDef {
            it.moduleId = _currentModule
            it.id       = scopeId
            it.threaded = threaded
        }
        _scopeDefs.add(scopeDef)
        return ScopeBuilderImpl { it.scopeDef = scopeDef }
    }

    ** Defines a new service to be added to the registry.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.addService(Penguin#).withId("penguins").withScope("root")
    ** <pre
    ** 
    ** Note if 'serviceType' is a mixin then, if none given, the builder looks for an 
    ** implementation class named the same as the mixin but with an 'Impl' suffix.
    ** 
    ** Hence:
    ** pre>
    ** syntax: fantom
    ** regBuilder.addService(Penguins#, PenguinsImpl#)
    ** <pre
    ** 
    ** is the same as:
    **  
    ** pre>
    ** syntax: fantom
    ** regBuilder.addService(Penguins#)
    ** <pre
    ** 
    ServiceBuilder addService(Type? serviceType := null, Type? serviceImplType := null) {
        _lock.check
        bob := ServiceBuilderImpl(_currentModule)
        _serviceDefs.add(bob.srvDef)
        if (serviceType != null)
            bob.withType(serviceType)
        if (serviceImplType != null)
            bob.withImplType(serviceImplType)
        return bob
    }

    ** Override values in an existing service definition.
    ** 
    ** The given id may be a service id to override a service, or an override id to override an override.
    **   
    ** pre>
    ** syntax: fantom
    ** regBuilder.addService(Penguins#).withCtorArgs(["fishLegs"])
    ** regBuilder.overrideService(Penguins#.qname).withCtorArgs(["fishHands"])
    ** <pre
    ** 
    ServiceOverrideBuilder overrideService(Str serviceId) {
        _lock.check
        serviceBuilder := ServiceOverrideBuilderImpl(_currentModule)
        serviceBuilder.ovrDef.serviceId = serviceId
        serviceBuilder.ovrDef.overrideId    = "${serviceId}.override"
        _overrideDefs.add(serviceBuilder.ovrDef)
        return serviceBuilder
    }

    ** Override values in an existing service definition.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.addService(Penguins#).withCtorArgs(["fishLegs"])
    ** regBuilder.overrideServiceType(Penguins#).withCtorArgs(["fishHands"])
    ** <pre
    ** 
    ServiceOverrideBuilder overrideServiceType(Type serviceType) {
        _lock.check
        serviceBuilder := ServiceOverrideBuilderImpl(_currentModule)
        serviceBuilder.ovrDef.serviceType   = serviceType
        serviceBuilder.ovrDef.overrideId    = "${serviceType}.override"
        _overrideDefs.add(serviceBuilder.ovrDef)
        return serviceBuilder
    }

    ** Lets you contribute to a service configuration via its ID.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.contributeToService("acme::Penguins") |Configuration config| {
    **     config["food"] = Fish()
    ** }
    ** <pre
    ** 
    ** The service ID you're contributing to has to exist. If the service may, or may not exist, 
    ** (for example, if you're contributing to an optional 3rd party library) then you may mark 
    ** the contribution as *optional*. That way, no Err is thrown should the service not exist.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.contributeToService("acme::Penguins", |Configuration config| {
    **     config["food"] = Fish()
    ** }, true)
    ** <pre
    ** 
    This contributeToService(Str serviceId, |Configuration| configFunc, Bool optional := false) {
        _contribDefs.add(ContribDef {
            it.moduleId         = _currentModule
            it.serviceId        = serviceId
            it.optional         = optional
            it.configFuncRef    = _toImmutableObj(Unsafe(configFunc))
        })
        return this
    }

    ** Lets you contribute to a service configuration by its Type.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.contributeToServiceType(Penguins#) |Configuration config| {
    **     config["food"] = Fish()
    ** }
    ** <pre
    ** 
    This contributeToServiceType(Type serviceType, |Configuration| configFunc, Bool optional := false) {
        _contribDefs.add(ContribDef {
            it.moduleId         = _currentModule
            it.serviceType      = serviceType
            it.optional         = optional
            it.configFuncRef    = _toImmutableObj(Unsafe(configFunc))
        })
        return this
    }
    
    ** Hook for executing funcs when the Registry starts up.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.onRegistryStartup |Configuration config| {
    **     config["log.hello1"] = |Scope rootScope| {
    **         penguin := rootScope.serviceById("acme::Penguin")
    **         Log.get("afIoc").info("Hello ${penguin.name}!")
    **     }
    ** }
    ** <pre
    ** 
    ** Note that the *root scope* is always passed into startup funcs.
    ** 
    ** An alternative to this method is to use an 'AppModule' method named 'onRegistryStartup()', 
    ** the advantage of which is that the arguments are dependency resolved.
    ** 
    ** pre>
    ** syntax: fantom
    ** Void onRegistryStartup(Configuration config, Penguin penguin) {
    **     config.set("log.hello2", |Scope rootScope| {
    **         Log.get("afIoc").info("Hello ${penguin.name}!")
    **     }).after("log.hello1")
    ** }
    ** <pre
    ** 
    ** Or, if you don't care about ordering, you may omit the call to 'Configuration':
    ** 
    ** pre>
    ** syntax: fantom
    ** Void onRegistryStartup(Penguin penguin) {
    **     Log.get("afIoc").info("Hello ${penguin.name}!")
    ** }
    ** <pre
    This onRegistryStartup(|Configuration| startupHook) {
        _registryStartupHooks.add([_currentModule, startupHook])
        return this
    }

    ** Hook for executing funcs when the Registry shuts down.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.onRegistryShutdown |Configuration config| {
    **     config["log.bye1"] = |Scope rootScope| {
    **         penguin := rootScope.serviceById("acme::Penguin")
    **         Log.get("afIoc").info("Goodbye ${penguin.name}!")
    **     }
    ** }
    ** <pre
    ** 
    ** Note that the *root scope* is always passed into shutdown funcs.
    ** 
    ** An alternative to this method is to use an 'AppModule' method named 'onRegistryShutdown()', 
    ** the advantage of which is that the arguments are dependency resolved.
    ** 
    ** pre>
    ** syntax: fantom
    ** Void onRegistryShutdown(Configuration config, Penguin penguin) {
    **     config.set("log.bye2", |Scope rootScope| {
    **         Log.get("afIoc").info("Goodbye ${penguin.name}!")
    **     }).after("log.bye1")
    ** }
    ** <pre
    ** 
    ** Or, if you don't care about ordering, you may omit the call to 'Configuration':
    ** 
    ** pre>
    ** syntax: fantom
    ** Void onRegistryShutdown(Penguin penguin) {
    **     Log.get("afIoc").info("Goodbye ${penguin.name}!")
    ** }
    ** <pre
    This onRegistryShutdown(|Configuration| shutdownHook) {
        _registryShutdownHooks.add([_currentModule, _toImmutableObj(shutdownHook)])
        return this
    }

    ** Hook for executing funcs when a scope is created.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.onScopeCreate("ro*") |Configuration config| {
    **     config["log.hello"] = |Scope scope| {
    **         Log.get("afIoc").info("BEGIN ${scope.id}")
    **     }
    ** }
    ** <pre
    ** 
    ** Where 'scopeGlob' is a not a scope ID, but a regex glob that is used to match 
    ** against scope IDs and their aliases.
    This onScopeCreate(Str scopeGlob, |Configuration| createHook) {
        _scopeCreateHooks.add([_currentModule, Regex.glob(scopeGlob), _toImmutableObj(createHook)])
        return this
    }

    ** Hook for executing funcs when a scope is created.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.onScopeDestroy("ro*") |Configuration config| {
    **     config["log.bye"] = |Scope scope| {
    **         Log.get("afIoc").info("END ${scope.id}")
    **     }
    ** }
    ** <pre
    ** 
    ** Where 'scopeGlob' is a not a scope ID, but a regex glob that is used to match 
    ** against scope IDs and their aliases.
    This onScopeDestroy(Str scopeGlob, |Configuration| destroyHook) {
        _scopeDestroyHooks.add([_currentModule, Regex.glob(scopeGlob), _toImmutableObj(destroyHook)])
        return this
    }

    ** Hook for executing funcs when a service is created.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.onServiceBuild("*penguin*") |Configuration config| {
    **     config["log.hello"] = |Obj? instance, Scope scope, ServiceDef def| {
    **         Log.get("afIoc").info("HELLO WORLD from ${def.id}")
    **     }
    ** }
    ** <pre
    ** 
    ** Where 'scopeGlob' is a not a service ID, but a regex glob that is used to match 
    ** against service IDs and their aliases.
    ** 
    ** Note there is also a hook available for when class is autobuilt - 
    ** see the src for 'AutoBuilder' for details.
    This onServiceBuild(Str serviceGlob, |Configuration| buildHook) {
        _serviceBuildHooks.add([_currentModule, Regex.glob(serviceGlob), _toImmutableObj(buildHook)])
        return this
    }

    ** Allows you to decorate / replace services with your own implementations.
    ** 
    ** pre>
    ** syntax: fantom
    ** regBuilder.decorateService("acme::MyService") |Configuration config| {
    **     config["alt-1"] = |Obj? serviceInstance, Scope scope, ServiceDef serviceDef->Obj?| {
    **         return MyServiceAlt(serviceInstance)
    **     }
    ** }
    ** <pre
    ** 
    ** Where 'scopeGlob' is a not a service ID, but a regex glob that is used to match 
    ** against service IDs and their aliases.
    ** 
    ** The difference between decorating and overriding, is that when decorating you are passed the 
    ** original instance. This makes it perfect for wrapping implementations with logging methods, 
    ** or other cross cutting / AOP style operations. 
    This decorateService(Str serviceGlob, |Configuration| decoratorHook) {
        _decoratorHooks.add([_currentModule, Regex.glob(serviceGlob), _toImmutableObj(decoratorHook)])
        return this
    }

    ** Sets a value in the 'options' map. 
    ** Returns 'this' so it may be used as a builder method.        
    This setOption(Str name, Obj? value) {
        options.set(name, value)
        return this
    }

    ** Builds and returns the registry; this may only be done once.  
    Registry build() {
        defaults := Str:Obj?[:] { it.caseInsensitive = true }.addAll([
            "afIoc.bannerText" : "Alien-Factory IoC v$typeof.pod.version",
        ])

        defaults.each |val, key| {
            optType := options[key]?.typeof
            if (optType != null && optType != val.typeof)
                throw IocErr(ErrMsgs.regBuilder_invalidRegistryValue(key, optType, val.typeof))
        }

        // set defaults but allow modules to alter / add to options
        options = defaults.setAll(options)
        registry := _buildHard
        
        _lock.lock
        return registry
    }
    
    // ---- Private Methods -----------------------------------------------------------------------

    private Void _addModulesFromPod(Pod pod, Bool addDependencies := true) {
        if (_pods.contains(pod)) {
            _logger.debug(ErrMsgs.regBuilder_podAlreadyAdded(pod))
            return
        }
        
        moduleTypeNames := (Str?) null
        try     moduleTypeNames = pod.meta.get("afIoc.module")
        catch   _logger.warn("WARNING: Pod ${pod.name} does not define any meta")
        
        if (moduleTypeNames != null)
            _addModulesFromTypeNames(moduleTypeNames)

        if (addDependencies) {
            pod.depends.each |depend| {
                dependency := Pod.find(depend.name)
                _addModulesFromPod(dependency, addDependencies)
            }
        }
    }
    
    private Void _addModulesFromTypeNames(Str? moduleTypeNames) {
        if (moduleTypeNames == null)
            return
        
        moduleTypeNames.split(',', true).each |moduleTypeName| {
            moduleType := Type.find(moduleTypeName)
            addModule(moduleType)
        }       
    }
    
    private Registry _buildHard() {

        // inspect modules, and keep inspecting until no more are added
        moduleTypes := Type[,]
        while (_modulesAdd.size > 0) {
            modules := _modulesAdd.dup
            
            // clear _modulesAdd so module inspectors can add to it
            _modulesAdd.clear
            
            modules.each |module| {
                moduleTypes.add(module.typeof)
                _moduleStack.push(module.typeof)
                try inspectors.each { it.inspect(this, module) }
                finally _moduleStack.pop
            }
        }
        
        // despite best efforts, an inspected module may remove / ignore a module that's already been inspected
        _scopeDefs              = _scopeDefs            .exclude { _modulesRemove.contains(it.moduleId) }
        _serviceDefs            = _serviceDefs          .exclude { _modulesRemove.contains(it.moduleId) }
        _overrideDefs           = _overrideDefs         .exclude { _modulesRemove.contains(it.moduleId) }
        _contribDefs            = _contribDefs          .exclude { _modulesRemove.contains(it.moduleId) }
        _registryStartupHooks   = _registryStartupHooks .exclude { _modulesRemove.contains(it[0]) }
        _registryShutdownHooks  = _registryShutdownHooks.exclude { _modulesRemove.contains(it[0]) }
        _scopeCreateHooks       = _scopeCreateHooks     .exclude { _modulesRemove.contains(it[0]) }
        _scopeDestroyHooks      = _scopeDestroyHooks    .exclude { _modulesRemove.contains(it[0]) }
        _serviceBuildHooks      = _serviceBuildHooks    .exclude { _modulesRemove.contains(it[0]) }
        _decoratorHooks         = _decoratorHooks       .exclude { _modulesRemove.contains(it[0]) }

        // we could use Map.addList(), but do it the long way round so we get a nice error on dups
        services := Str:SrvDef[:] { caseInsensitive = true }
        _serviceDefs.each {
            if (services.containsKey(it.id))
                throw IocErr(ErrMsgs.regBuilder_serviceAlreadyDefined(it.id, it.moduleId, services[it.id].moduleId))
            services[it.id] = it
        }

        // we could use Map.addList(), but do it the long way round so we get a nice error on dups
        overrides   := Str:OvrDef[:] { caseInsensitive = true}
        _overrideDefs.each |ovr| {
            if (ovr.serviceId == null) 
                ovr.serviceId = services.find { it.matchesType(ovr.serviceType) }?.id ?: ErrMsgs.scope_couldNotFindServiceByType(ovr.serviceType, null)
            if (overrides.containsKey(ovr.serviceId))
                throw IocErr(ErrMsgs.regBuilder_onlyOneOverrideAllowed(ovr.serviceId, ovr.moduleId, overrides[ovr.serviceId].moduleId))
            overrides[ovr.serviceId] = ovr
        }

        keys := Str:Str[:] { it.caseInsensitive = true }
        services.keys.each { keys[it] = it }

        // normalise keys -> map all keys to orig key and apply overrides
        // code nabbed from Configuration
        found := true
        while (overrides.size > 0 && found) {
            found = false
            overrides = overrides.exclude |over, existingId| {
                overrideId := over.overrideId
                if (keys.containsKey(existingId)) {
                    if (keys.containsKey(overrideId))
                        throw IocErr(ErrMsgs.regBuilder_overrideAlreadyDefined(over.overrideId, over.moduleId, services[keys[existingId]].moduleId))

                    keys[overrideId] = keys[existingId]
                    found = true
                    
                    srvDef := services[keys[existingId]]                        
                    srvDef.applyOverride(over)

                    return true
                } else {
                    return false
                }
            }
        }

        overrides = overrides.exclude { it.optional }

        if (!overrides.isEmpty) {
            keysNotFound := overrides.keys.join(", ")
            throw ServiceNotFoundErr(ErrMsgs.regBuilder_serviceIdNotFound(keysNotFound), services.keys)
        }

        _contribDefs.each |contribDef| {
            // should only really ever be the one match!
            matches := _serviceDefs.findAll |srvDef| { 
                contribDef.matches(srvDef)
            }
            matches.each {
                it.addContribDef(contribDef)
            }

            if (!contribDef.optional && matches.isEmpty)
                throw ServiceNotFoundErr(ErrMsgs.contributionServiceNotFound(contribDef.srvId, contribDef.method2), _serviceDefs)
        }

        // we could use Map.addList(), but do it the long way round so we get a nice error on dups
        scopeDefs := Str:ScpDef[:] { caseInsensitive = true }
        _scopeDefs.each |def| {
            if (scopeDefs.containsKey(def.id))
                throw IocErr(ErrMsgs.regBuilder_scopeAlreadyDefined(def.id, def.moduleId, scopeDefs[def.id].moduleId))
            scopeDefs[def.id] = def
        }
        
        _scopeCreateHooks.each |hook| {
            matches := scopeDefs.any |scpDef| { scpDef.matchesGlob(hook[1]) }
            if (!matches)
                throw IocErr(ErrMsgs.regBuilder_scopeNotMatched("onScopeCreate:", hook[1], hook[0].toStr, scopeDefs.vals.sort |s1, s2| { s1.id <=> s2.id }))
        }

        _scopeDestroyHooks.each |hook| {
            matches := scopeDefs.any |scpDef| { scpDef.matchesGlob(hook[1]) }
            if (!matches)
                throw IocErr(ErrMsgs.regBuilder_scopeNotMatched("onScopeDestroy:", hook[1], hook[0].toStr, scopeDefs.vals.sort |s1, s2| { s1.id <=> s2.id }))
        }

        _serviceBuildHooks.each |hook| {
            matches := services.any |srvDef| { srvDef.matchesGlob(hook[1]) }
            if (!matches)
                throw ServiceNotFoundErr(ErrMsgs.regBuilder_serviceNotMatched("onServiceBuild:", hook[1], hook[0].toStr), services.vals)
        }
        
        _decoratorHooks.each |hook| {
            matches := services.any |srvDef| { srvDef.matchesGlob(hook[1]) }
            if (!matches)
                throw ServiceNotFoundErr(ErrMsgs.regBuilder_serviceNotMatched("decorateService:", hook[1], hook[0].toStr), services.vals)
        }
        
        scopeDefs.each |ScpDef scpDef| {
            scpDef.createContribs  = _scopeCreateHooks .findAll { scpDef.matchesGlob(it[1]) }.map { it[2] }
            scpDef.destroyContribs = _scopeDestroyHooks.findAll { scpDef.matchesGlob(it[1]) }.map { it[2] }
            if (scpDef.createContribs.isEmpty)
                scpDef.createContribs = null
            if (scpDef.destroyContribs.isEmpty)
                scpDef.destroyContribs = null
        }

        services.each |srvDef| {
            srvDef.buildHookContribs    = _serviceBuildHooks .findAll { srvDef.matchesGlob(it[1]) }.map { it[2] }
            srvDef.decorateHookContribs = _decoratorHooks    .findAll { srvDef.matchesGlob(it[1]) }.map { it[2] }
            if (srvDef.buildHookContribs.isEmpty)
                srvDef.buildHookContribs = null
            if (srvDef.decorateHookContribs.isEmpty)
                srvDef.decorateHookContribs = null
        }
        
        return RegistryImpl(_buildStart, scopeDefs, services, moduleTypes, options, _registryStartupHooks.map { it[1] }, _registryShutdownHooks.map { it[1] })
    }
    
    private Obj? _toImmutableObj(Obj? obj) {
        if (obj is Func)
            return Env.cur.runtime == "js" ? obj : obj.toImmutable
        return obj?.toImmutable
    }
    
    private Type _currentModule() {
        _moduleStack.peek ?: RegistryBuilder#
    }
}