require 的行为(静态 + 动态)[ RAKU ]

Behaviour of require (static + dynamic) [ RAKU ]

我的问题与 require 与所需命名空间的静态或动态解析一起使用时的行为有关。

我将尝试展示我对事物的理解:

[ 1 ] 使用 "require" 和文字

    { require MODULE; }

在这种情况下,编译器会检查是否已将 MODULE 声明为符号。 如果没有,编译器会声明它,并将它绑定到一个空的占位符包,它是为此 "require"

创建的
{
    my $response = ::('MODULE');  # this happens at runtime
    say $response.^name;          # MODULE doesn't exist so the lookup results in the compilation-phase placeholder package: MODULE

    try require MODULE;           # although the execution order of require comes after the lookup, 
                                  # the placeholder package creation was done during compilation and the package is present in the current scope during run-time
}

[ 2 ] 将 "require" 与字符串

一起使用
    { try require 'FILE_PATH'; }

在这种情况下,"require" 试图找到(在 运行 时间)由字符串中声明的文件名定义的文件。 如果找到(具有适当的内容:模块、包等),则它会在当前范围内创建一个命名空间,并使用文件的内容加载它。

[ 3 ] 使用 "require" 进行动态查找

    { try require ::('MODULE'); }

在我看来,在那种情况下 "require" 的行为 NOT 作为 "normal" 子例程。

当我们将 "require" 与 "dynamic lookup" 一起使用时,动态查找的核心功能是 "melted" 在一个新例程中,它的行为与我们预期的不同。

事实是 "dynamic lookup" 例程的结果是符号或失败。

如果 "require" 的行为类似于 "normal" 子例程,那么它唯一可以使用的输入将是跟随它的动态查找的结果(命名空间或失败)。

但事实是,在失败的情况下(作为动态查找的结果),"require" 继续在存储库中搜索合适的包(通常情况下,仍然使用我们给动态查找的参数:'MODULE').

很明显 "require" 在这个意义上不像 "normal" 子例程。

根据我的思路,require + 动态查找的组合类似于以下结构:

{ modified_dynamic_lookup('MODULE') :if_symbol_not_found_search_repositories_and_if_appropriate_package_found_create_namespace_and_load_package_contents; }

我担心的是我对案例[3]的理解。

require + 动态查找是如何工作的? (从分析上来说-编译器首先遵循的步骤是什么,然后运行时间?)

[Post 脚本]

我同意@raiph 的观点,"require" 不是 子例程,并且它已深入集成到语言中。

从这个意义上讲,要求 "instruction" 之后的 "dynamic lookup construct" 用于两件事:

  1. 通知编译器构造是"dynamic"(所以不要 麻烦在编译时修复任何东西)

  2. 提供将用于搜索符号的字符串, 名称空间、文件或存储库内容

@raiph 表示他认为 "require" 在成功加载后进行查找。

我唯一的反对意见是当我们加载同一个库时 "require" 不会抛出任何异常。

是否默默忽略加载的库? 既然可以先检查同一个命名空间是否已在使用,为什么还要费心做这么多工作呢?

相反,当我们假装我们加载了一个不同的库时,它会抛出一个异常:"duplicate definition" of the symbol in use.

为了证明我进行了以下操作:

在 ./lib 目录中,我放置了两个库,"foo.pm6" 是 "foo" 的单元定义,其中定义了 class A:

file "foo.pm6" contents:
-----------------------------------
unit module foo;

class A is export {}

和另一个库 "other.pm6",这次在内部定义了 "foo",其中定义了不同的 class B。

file "other.pm6" contents:
-----------------------------------
module foo {
    class B is export {}
}

raku 程序文件包含以下内容:

use lib <lib>;

my $name = 'other';           # select one of {'other', 'foo'}

require ::('foo') <A>;        ########> Initial package loading

my $a = try ::('foo::A').new;
say '(1) ' ~ $a.^name;        # (1) foo::A

$a = ::('A').new;
say '(2) ' ~ $a.^name;        # (2) foo::A

try require ::($name);        # if $name eq 'other' => throws exception, if $name eq 'foo' => does nothing
with $! {.say};               # P6M Merging GLOBAL symbols failed: duplicate definition of symbol foo ...

$a = try ::('foo::A').new;
say '(3) ' ~ $a.^name;        # (3) foo::A

$a = ::('A').new;
say '(4) ' ~ $a.^name;        # (4) foo::A

从上面的示例中我们看到,当我们尝试重新加载 foo 命名空间时,它隐藏在一个具有不同名称的文件中(只是为了欺骗 raku),它会引发异常。

因此我得出结论,也许 "require" 首先检查与提供的字符串同名的命名空间。

顺便说一句,检查这个时,我偶然发现了一个奇怪的行为。内容如下:

如果我们在行中使用 "use foo;":"Initial package loading" 而不是 "require ::('foo') ;",我们会得到以下结果:

(1) foo::A
(2) foo::A
No such symbol 'other' ...

(3) Any
(4) foo::A

在 (3) 中 'foo::A' 的查找未找到任何内容!!!

此外,如果我更改库文件:"other.pm6" 使用以下内容(class A 而不是 B - 如 foo.pm6 中所示)

file "other.pm6" contents:
-----------------------------------
module foo {
    class A is export {}
}

结果似乎恢复到预期:

(1) foo::A
(2) foo::A
No such symbol 'other' ...

(3) foo::A
(4) foo::A

这是错误还是我遗漏了什么?

重写以对应 the third version 你的答案。

[ 1 ] 使用带有文字的“require”

In this case the compiler checks to see if MODULE has already been declared as a symbol. If it hasn't, the compiler declares it, and binds it to an empty placeholder package it's just created for this "require"

更具体一点,require 关键字,以及它生成的代码4,工作。

它创建符号的唯一原因是可以编写该标识符并且代码可以编译。如果 require 没有这样做,那么使用标识符的代码将无法编译,即使 require FOO 会成功:

require FOO;
my FOO $bar; # Type 'FOO' is not declared

# MODULE doesn't exist so the lookup results in the compilation-phase placeholder package: MODULE

MODULE 存在。查找成功。它returns绑定到MODULE符号的值,这是require在编译阶段放在那里的占位符包。

# although the execution order of require comes after the lookup

require 的编译阶段动作的执行发生在 运行 阶段发生的查找之前。

[ 2 ] 对字符串使用“require”**

If found (with appropriate content: modules, packages etc.) then it creates a namespace(s) in the current scope and loads it with the content(s) of the file.

我认为符号 require 所做的唯一声明是代码编写者明确写为静态标识符作为 require 语句的一部分的那些。示例:

  • require MODULE <A>; --> MODULEA.

  • require 'MODULE.pm6' <A>; --> A.

  • require ::('MODULE') <A>; --> A.

Aiui MLS1,作为符号 merging (P6M) 的一部分,根据需要声明更多符号。但是这项工作并不是由 require 完成的。它由 MLS 代表它完成。这并不是 require 所特有的。它与 use 语句在编译阶段发生的相同(某种)工作。

[ 3 ] 对动态查找使用“require”

{ try require ::('MODULE'); }

我有代码试图证明这 不会 在尝试加载模块之前进行查找。2

It seems to me that in that case "require" behaves NOT as a "normal" subroutine.

require 不是例程,正常或其他。

say require MODULE;   # Undeclared name:
                            MODULE used at line 1
                      # Undeclared routine:
                            require used at line 1

如果您在 the official doc 中搜索 require,您会发现不是 例程参考 ] 部分,而不是 语言参考 的模块部分。它是关键字,语句,编译器理解的语言的特殊部分。

If "require" behaves like a "normal" subroutine, then the only input it could use, would be the result of the dynamic lookup that followed it (Namespace or Failure).

动态查找的结果是绑定到 Symbol 的值,如果它被声明,或者 Failure 否则:

my $variable = 42;
say ::('$variable');           # 42
say ::('nonsense') ~~ Failure; # True

$variable 不是命名空间。

But it is also a fact that in the case of a Failure (as the result of dynamic lookup), "require" continues searching the repositories for a proper package (as is normally the case, using nevertheless the argument we gave to dynamic lookup: 'MODULE').

鉴于我编写的跟踪动态查找 ::('MODULE')2 的代码,在我看来很可能没有动态查找它通过任何代码,无论是 require 还是 MLS,如果模块加载 失败 .

这反过来意味着它只会发生,如果有的话,(成功)加载模块期间或之后。因此,要么是 MLS 正在这样做(似乎最有可能),要么是 require 模块成功加载后正在这样做(似乎不太可能,但我不是但准备 100% 消除它)。

{ modified_dynamic_lookup('MODULE') :if_symbol_not_found_search_repositories_and_if_appropriate_package_found_create_namespace_and_load_package_contents; }

我想我已经证明 require 或 MLS 根本没有查找,或者,如果查找了,也只是 after模块已成功加载。

what are the steps followed by the compiler at first and then by the runtime?

这个答案当然是试图回答这个问题,但我简短的编译器代码分析可能会有所帮助。3(虽然单击 link 可以看到Actions.nqp 中的实际代码不适合胆小的人!)

[Post手稿]

In that sense the "dynamic lookup construct" that follows the require "instruction" is used for 2 things:

  1. To notify the compiler that the construct is "dynamic" (so don't bother fixing anything at compile time)

  2. To provide the string that will be used to search for symbols, namespaces, files or repository content

我认为它只做 2,只是传递给 MLS 的包名称。

when we load the same library "require" doesn't throw any exception. Is it silently ignoring the loaded library?

我认为 require 对此一无所知。它把它交给 MLS,然后在 MLS 完成它的事情后接手。我不认为 require 可以区分 MLS 何时成功执行新加载和何时跳过加载。它只知道 MLS 是说一切正常还是有异常。

Why bother doing so much work when it can check first that the same namespace is already in use?

既然 MLS 已经在做 任何 工作,而且 require 无论如何都会调用 MLS,为什么还要费心去做呢?做任何事都是徒劳。

所有 require 所要做的就是处理用户在 require 语句中明确键入的 compile-phase 符号。它不能要求MLS处理这些,因为它与成功模块加载无关,这是唯一的场景MLS 去摆弄符号。

In contrary when we pretend that we load a different library then it throws an Exception : "duplicate definition" of the symbol in use.

试试这个:

require ::('foo');
require ::('other');

现在将foo.pm6other.pm6中的unit module foo;改为unit module bar;再试一次。您仍然会得到相同的异常,但符号将为 barrequire 怎么知道 bar?它不能。异常来自 MLS,并且只有 MLS 知道该符号。

Therefore I conclude that maybe "require" checks first for a namespace that has the same name as the provided string.

除非你将 MLS 算作 require 的一部分,否则我相信你现在可以看到你的“可能”资格是明智的。 :)

I stumbled upon a strange behaviour ... The lookup of 'foo::A' in (3) doesn't find anything !!!

我有一个解释。我并不是说它是对的,但在我写这篇文章时,它似乎 不奇怪:

use 语句加载 foo.pm6 包。它定义了一个包 foo,其中包含一个 class A,并导出 A。这导致导入词法范围 foo 中的一个符号绑定到一个包,该包包含一个符号 A。它还会在导入词法范围内产生另一个符号,A.

require 语句加载 other.pm6 包。它定义了一个包 foo,其中包含一个 class B,并导出 B。这导致将导入词法范围中的 foo 符号重新绑定到不同的包,即包含符号 B 的新包。它还会在导入词法范围内产生另一个符号,B.

较早的 A 闲逛。 (换句话说,P6M 符号合并过程不包括 删除 符号。)但是 foo::A,它在绑定到 foo 符号的包中查找, 不再存在,因为绑定到 foo 符号的包现在是 other.pm6 包中的包,覆盖了 foo.pm6 包中的包。

与此同时还有一个奇怪的事情:

try require ::($name);
with $! {.say};             # No such symbol 'other' ...

我认为这反映了require成功模块加载后进行(失败)查找。

请注意,如果模块加载失败,此消息不会出现;这似乎再次证实了我的想法(和代码2),即 require 在成功加载之前不会进行任何查找(如果那样;我仍然没有强大的了解是 MLS 在做这些事情还是 require;代码4 对我来说太复杂了 atm)。

对您的评论的回复

根据您对此答案的评论:

Its like we get as the result of the amalgamation of require + 'dynamic lookup formulation' an enhanced dynamic lookup like this { ::('something') :if_not_found_as_namespace_check_repositories_and_load }

出于各种原因,这对我来说并不正确。

例如,假设有一个包 foo 声明为 module foo { our sub bar is export { say 99 } },如果 required 将成功加载。现在考虑这段代码:

my \foo = 42;
say ::('foo');             # 42
require ::('foo') <&bar>;
say foo;                   # 42
bar;                       # 99

这对我来说很有意义。它不会加载名称为 42 的包。它不会查找符号 foo。它将加载名称为 foo 的包。虽然它可能会在 加载包后查找符号 foo ,但它 不会 安装符号 foo 因为已经有一个了。

脚注

1 By Module Loading Subsystem 我的意思是各个部分,给定一个模块名称,做一些事情,比如搜索本地文件系统,或数据库,检查预编译目录,调用编译,并在模块成功加载时合并符号。我不知道部件之间以及部件和编译器之间的界限在哪里。但我相信它们 不是 require 的一部分,而只是被它调用。


2 运行 这段代码:

my \MODULE =
  { my $v;
    Proxy.new:
      FETCH => method { say "get name: $v"; $v },
      STORE => method ($n) { say "set name: $n"; $v = $n }}();

MODULE = 'unseen by `require`';
say ::('MODULE');

use lib '.';
say 'about to `require`';
require ::('MODULE');

3 我们从 the relevant match in Raku's Grammar.nqp file:

开始
  rule statement_control:sym<require> {
        <sym>
        [
        | <module_name>
        | <file=.variable>
        | <!sigil> <file=.term>
        ]
        <EXPR>?
    }

代码似乎符合我们的预期——require 关键字后跟:

  • 一个包标识符(<module_name>);或

  • a <variable>(例如$foo);或

  • 不以 <sigil>.

    开头的 <term>

我们对 <module_name> 分支感兴趣。它调用 token module_name 调用 token longname 调用 token name:

token name {
        [
        | <identifier> <morename>*
        | <morename>+
        ]
}

显然 ::('foo') 不是以 <identifier> 开头的。所以它是 token morename。删几句没意思的就走了:

    token morename {
        '::'
        [
        ||  <?before '(' | <.alpha> >
            [
            | <identifier>
            | :dba('indirect name') '(' ~ ')' [ <.ws> <EXPR> ]
            ]
        ]?
    }

宾果游戏。这将匹配 ::(,特别是 :dba('indirect name') '(' ~ ')' [ <.ws> <EXPR> ] 位。

所以此时我们将捕获:

statement_control:sym<require><module_name><longname><name><morename><EXPR>

稍后 statement_control:sym<require> 令牌即将成功。所以此时它会调用Actions.nqp...

中对应的action方法

4Actions.nqp中我们找到token statement_control:sym<require>对应的动作,即method statement_control:sym<require>。开头的 if $<module_name> { 条件将是 True,导致 运行 宁此代码:

$longname := $*W.dissect_longname($<module_name><longname>);
$target_package := $longname.name_past;

在我看来,这段代码正在剖析解析 ::('foo') 的结果,并将对应于该剖析的 AST 绑定到 $target_package,而不是费心进行查找或准备 运行-时间查找。

如果我是对的,::('foo') 不需要超过 9 个字符,require 可以解释,但它喜欢解释它们。这里没有必要暗示它在构造包加载代码时会做任何特定的事情,例如查找。


动作的后半部分进行查找。有像 this:

这样的行
?? self.make_indirect_lookup($longname.components())

并给出例程名称,我认为 进行查找,可能是 require 在包加载成功时尝试添加包符号的一部分.

require 如果可以的话,在编译期间做一些事情。

require Module;
say Module;

它假定加载该模块将为您提供名称为 Module 的内容。

因此它会在编译时安装一个具有该名称的临时符号。

这是它在编译时唯一做的事情。
(所以当我说“有些事”时我撒谎了。)

if Bool.pick {
    require module-which-does-not-exist;

    module-which-does-not-exist.method-call()
}

大约有一半时间以上内容什么都不做。
另一半时间它在 运行 时抱怨找不到模块。

(我选择了 Bool.pick 而不是 False 所以编译时优化器肯定无法优化它。)


当你用标识符以外的东西调用它时,它在编译时不知道模块是什么。 所以它不能创建临时命名空间。

require 'Module';
say Module; # COMPILE ERROR: undeclared name
require Module; # RUNTIME ERROR: can't find 'Module'
say Module;
require 'Module'; # RUNTIME ERROR: can't find 'Module'
say ::('Module');
if False {
    require Module;
    say Module;
}
# no error at all
if False {
    require 'Module';
    say ::('Module');
}
# no error at all