首页 > 解决方案 > “范围”或“上下文”如何在编译程序中存储和引用?

问题描述

抱歉,如果我混淆了“范围”和“上下文”这两个术语,但基本上我指的是我认为的词法范围,但是当函数(或类主体)正在评估时它的实例。

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

同样,我想为模块、函数和其他一些东西实现这个,比如我的视图/组件(我不想成为像 React 这样的函数,类似地,我的语言中的模块不是评估函数)。在 JavaScript 中,您只有被调用的函数,并且范围以某种方式存储在每个函数中。

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

我想象的是将模块包装在实际上是一个“树”对象中,例如:

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

这里的tree对象是某种 AST 包装器对象,它具有对 上使用的范围的引用object,以及对象本身。这是为了防止对象被它没有的额外scope属性污染:模块没有一般意义上的范围属性。模块的解析是有作用域的,所以也许树对象实际上被称为解析对象。

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

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

所以我想象做的是把这个解析对象称为“叉子”,本质上是构建一个数据树。每个 fork 对象都有一个 scope 属性,每个对象实际上都包含在一个 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”对象之类的东西中,就像我在这里尝试做的那样?

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

标签: javascriptdata-structuresscopeprogramming-languagesabstract-syntax-tree

解决方案


V8 通过一个在AST 节点以及所有其他引入自己的范围的节点中Scope引用名为的类来表示范围,例如,或.BlockWithTryCatchFunctionExpression

例如,当 v8 编译 JavaScript 时,每个 AST 对象是否都有对其作用域对象的引用?因此,每个 AST 对象是否都包装在“scopeResolver”对象之类的东西中,就像我在这里尝试做的那样?

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

据我所见,V8 解释器将作用域保留在ContextScope链表内。


推荐阅读