"scope" 或 "context" 如何在已编译程序中存储和引用?

How is "scope" or "context" stored and referenced in a compiled program?

抱歉,如果我混淆了“作用域”和“上下文”这两个术语,但基本上我指的是我认为的词法作用域,但是当函数(或 class body) 正在评估。

我正在致力于实现一种奇怪的基于树的编程语言,并且具有需要某种“作用域”的不同结构。类似的情况出现在我熟悉的 2 种语言中,JavaScript 和不太熟悉的 Ruby。在 Ruby 中,您有可执行的 class 主体,因此它们有自己的范围,但是您也有具有自己范围的可执行函数。然后在块内部,它们每个都有自己的范围。 “词法”范围树基本上是 variables/stuff 树,您可以在父树的可见代码中引用(可见范围是 词法 范围)。

同样,我想为模块、函数和其他一些东西实现这个,比如我的 views/components(我不想像 React 那样的函数,类似地,我的 lang 中的模块没有被评估职能)。在 JavaScript 中,您只有被调用的函数,并且范围以某种方式存储在每个函数中。

但我想知道的是,什么是数据模型/数据结构/或通常是如何存储和引用范围的“结构”? 字面意思是AST 或任何地方,它是如何工作的?

我想做的是将一个模块包裹在一个“树”对象中,例如:

{
  type: 'tree',
  scope: {
    foo: 'bar',
  },
  object: module
}

这里的 tree 对象是某种 AST 包装器对象,它引用了 object 上使用的范围和对象本身。这是为了防止对象被额外的 scope 属性 污染,它没有:一个模块在一般意义上没有作用域 属性。模块的resolution是有作用域的,所以说不定树对象其实就是resolution对象

{
  type: 'resolution',
  scope: {
    foo: 'bar',
  },
  object: module
}

但随后它开始变得毛茸茸。在模块中有 classes,它们有一个 class 作用域和一个实例作用域。或具有实例范围的函数。或者我正在使用的不同自定义对象中的其他范围树。

所以我想做的是将这个解析对象称为“叉子”,并且实质上是构建一个 数据树 。每个 fork 对象都有一个作用域 属性,每个对象实际上都包裹在一个 fork.

const moduleFork = {
  type: 'fork',
  scope: {
    foo: 'bar',
  },
  children: {
    // this is a module now.
    functions: {
      type: 'fork',
      children: {}
    }
  }
}

然后在 functions 对象中,我们有一个函数示例:

// the functions scope is the module itself.
moduleFork.children.functions.scope = moduleFork.children

// then a function func1
moduleFork.children.functions.func1 = {
  // this is inside the function now.
  type: 'fork',
  scope: moduleFork.children,
  children: {
    params: {
      type: 'fork',
      list: true,
      children: {
        param1: {
          type: 'fork',
          children: {
            name: {
              type: 'string',
              value: 'param1'
            },
            default: {
              type: 'string',
              value: 'hello world'
            }
          }
        }
      }
    }
  }
}

我不知道,类似的东西,我还在努力。但是因此,AST 中的每个对象都有一个对范围的引用,它可以用来获取变量值。我省略了一些重复,但实际上它或多或少看起来像这样:

// then a function func1
{
  // this is inside the function now.
  type: 'fork',
  scope: moduleFork.children,
  children: {
    params: {
      type: 'fork',
      list: true,
      scope: moduleFork.children,
      children: {
        param1: {
          type: 'fork',
          scope: moduleFork.children,
          children: {
            name: {
              type: 'string',
              value: 'param1'
            },
            default: {
              type: 'string',
              value: 'hello world'
            }
          }
        }
      }
    }
  }
}

那样的话,您可以 getFromTree(astNode.scope, 'foo') 并且很简单。同样,由于我们在模块的上下文中,我们可以执行 getFromTree(paramNode.scope, 'functions') 并且它将从模块中获取函数数组(从 fork 包装器之类的东西反序列化)。

在我的语言中,一些 AST 节点会像 { type: 'reference', path: ['foo'] },在这种情况下,这个对象知道范围很重要,这样它才能解析 foo。因此,拥有“包装叉”概念似乎可以轻松传递作用域,尤其是当您拥有事件和数据绑定之类的东西时。而不是像 JavaScript 中那样具有实际的嵌套绑定函数作用域,您只是在处理 AST 树对象。

关键问题是,这在其他编程语言实现中是如何完成的?例如,当 v8 编译 JavaScript 时,是否每个 AST 对象都有对其作用域对象的引用?因此,是否每个 AST 对象都包裹在一个“scopeResolver”对象之类的东西中,就像我在这里尝试做的那样?

如果可以的话,请画出作用域如何工作的数据结构的基本图片,以及它是否像我在这里尝试做的那样在每个对象中都被引用。

V8 在 Block AST 节点中通过一个名为 Scope which is referenced 的 class 表示范围,以及在所有其他节点中引入自己的范围,例如 With, TryCatchFunctionExpression.

For example, when v8 compiles JavaScript, does every AST object have a reference to its scope object? As such, is each AST object sort of wrapped in a "scopeResolver" object sort of thing, like I am trying to do here?

没有也没有。在解释和编译期间,AST 树总是以深度优先的方式遍历,因此当访问读取或写入变量的节点时,已经遍历了父块。因此,在遍历期间将父块存储在某种堆栈中似乎更容易,而不是在 AST 中添加对每个节点的另一个引用。特别是对于手动内存管理,这似乎是一个没有任何好处的大开销。

据我所知,V8 解释器将作用域保持在 ContextScope linked list.