Names, Scopes, and Bindings

A name is a mnemonic character string used to represent something else.

Names allow us to refer to variables, constants, operations, types, and so on using symbolic identifiers rather than low-level concepts like addresses.

Subroutines are control abstractions.

Classes are data abstractions.

The Notion of Binding Time

the notion of binding time, which refers not only to the binding of a name to the thing it represents, but also in general to the notion of resolving any design decision in a language implementation.

通常,早期绑定时机与更高的效率相关,而后期的绑定时机与更大的灵活性相关。

不同的东西的绑定时机是不一样的:

  • Language design time:控制流结构,基本类型,复杂对象组织方法等语义方面的内容
  • Language implementation time:基础类型大小,和操作系统交互,堆和栈的组织方式和大小
  • Program writing time:算法,数据结构,命名
  • Compile time:高级数据结构和机器码的映射,静态数据的内存布局
  • Link time:引用其他的模块的绑定关系到链接时才能确定(增量编译)
  • Load time:程序装载时才能确定实际的地址(虚实地址转换)
  • Run time:变量值绑定,程序启动时机,模块装载时机,首次“看到”声明的时机,子程序调用时机,代码块进入时机,表达式求值、语句执行时机

Compiler-based language implementations tend to be more efficient than interpreter-based implementations because they make earlier decisions.

Object Lifetime and Storage Management

The period of time between the creation and the destruction of a name-to-object binding is called the binding’s lifetime.

生命周期管理不正确肯能会导致“悬挂指针”

对象的生命周期取决于存储分配机制(对象空间):

  • 静态对象(绝对地址)
  • 栈对象(栈上分配,通常在子程序调用)
  • 堆对象(随时分配)

Static Allocation

Global variables are the obvious example of static objects, but not the only one.

Numeric and string-valued constant literals are also statically allocated.

Finally, most compilers produce a variety of tables that are used by run-time support routines for debugging, dynamic type checking, garbage collection, exception handling, and other purposes; these are also statically allocated.

Manifest constants can always be allocated statically, even if they are local to a recursive subroutine: multiple instances can share the same location.

Stack-Based Allocation

If a language permits recursion, static allocation of local variables is no longer an option.

Fortunately, the natural nesting of subroutine calls makes it easy to allocate space for locals on a stack.

Each instance of a subroutine at run time has its own frame (also called an activation record) on the stack, containing arguments and return values, local variables, temporaries, and bookkeeping information.

Heap-Based Allocation

A heap is a region of storage in which subblocks can be allocated and deallocated at arbitrary times.

Heaps are required for the dynamically allocated pieces of linked data structures, and for objects such as fully general character strings, lists, and sets, whose size may change as a result of an assignment statement or other update operation.

The principal concerns are speed and space, and as usual there are tradeoffs between them.

堆内存管理方法:

  • With a first fit algorithm we select the first block on the list that is large enough to satisfy the request.
  • With a best fit algorithm we search the entire list to find the smallest block that is large enough to satisfy the request.

两者的对比:

Intuitively, one would expect a best fit algorithm to do a better job of reserving large blocks for large requests. At the same time, it has higher allocation cost than a first fit algorithm, because it must always search the entire list, and it tends to result in a larger number of very small “left-over” blocks.

内存管理存在的问题:

内存分配效率和堆最小大小有关(多次申请):

In effect, the heap is divided into “pools,” one for each standard size. The division may be static or dynamic. Two common mechanisms for dynamic pool adjustment are known as the buddy system and the Fibonacci heap.

内存碎片问题:

The problem with external fragmentation is that the ability of the heap to satisfy requests may degrade over time.

Garbage Collection

The run-time library for such a language must then provide a garbage collection mechanism to identify and reclaim unreachable objects.

手动 vs 自动:

  • The traditional arguments in favor of explicit deallocation are implementation simplicity and execution speed.
  • manual deallocation errors are among the most common and costly bugs in real-world programs.

Scope Rules

The textual region of the program in which a binding is active is its scope. In most modern languages, the scope of a binding is determined statically, that is, at compile time.

作用域分为:

  • statically scoped: compile time
  • dynamically scoped: bindings depend on the flow of execution at run time

At any given point in a program’s execution, the set of active bindings is called the current referencing environment. The set is principally determined by static or dynamic scope rules.

binding rules:

  • deep binding: the choice is made when the reference is first created
  • shallow binding: the choice is made when the reference is finally used

Static Scoping

In a language with static (lexical) scoping, the bindings between names and objects can be determined at compile time by examining the text of the program, without consideration of the flow of control at run time.

Nested Subroutines

a name that is introduced in a declaration is known in the scope in which it is declared, and in each internally nested scope, unless it is hidden by another declaration of the same name in one or more nested scopes.

To find the object corresponding to a given use of a name, we look for a declaration with that name in the current, innermost scope. If there is one, it defines the active binding for the name. Otherwise, we look for a declaration in the immediately surrounding scope.

A name-to-object binding that is hidden by a nested declaration of the same name is said to have a hole in its scope.

作用域解析运算符:

In others, the programmer can access the outer meaning of a name by applying a qualifier or scope resolution operator.

Declaration Order

Put another way, can an expression E refer to any name declared in the current scope, or only to names that are declared before E in the scope?

Several early languages, required that all declarations appear at the beginning of their scope.

C++ and Java further relax the rules by dispensing with the define-before-use requirement in many cases. In both languages, members of a class (including those that are not defined until later in the program text) are visible inside all of the class’s methods.

Declarations and Definitions

如何处理两个类互相包含彼此?

Recursive types and subroutines introduce a problem for languages that require names to be declared before they can be used: how can two declarations each appear before the other?

  • A declaration introduces a name and indicates its scope, but may omit certain implementation details.
  • A definition describes the object in sufficient detail for the compiler to determine its implementation.

Modules

模块化和信息隐藏,减少认识负荷:

This modularization of effort depends critically on the notion of information hiding, which makes objects and algorithms invisible, whenever possible, to portions of the system that do not need them.

Module Types and Classes

An alternative solution to the multiple instance problem appeared in Euclid, which treated each module as a type. Given a module type, the programmer could declare an arbitrary number of similar module objects.

Dynamic Scoping

In a language with dynamic scoping, the bindings between names and objects depend on the flow of control at run time, and in particular on the order in which subroutines are called.

为什么动态作用域到运行时才能确定?

Because the flow of control cannot in general be predicted in advance, the bindings between names and objects in a language with dynamic scoping cannot in general be determined by a compiler.

Implementing Scope

To keep track of the names in a statically scoped program, a compiler relies on a data abstraction called a symbol table.

In a language with dynamic scoping, an interpreter (or the output of a compiler) must perform operations analogous to symbol table insert and lookup at run time.

The Meaning of Names within a Scope

A name that can refer to more than one object at a given point in the program is said to be overloaded. Overloading is in turn related to the more general subject of polymorphism.

  • aliases: Two or more names that refer to the same object at the same point in the program are said to be aliases.
  • overloaded: A name that can refer to more than one object at a given point in the program is said to be overloaded
  • Redefining Built-in Operators

The Binding of Referencing Environments

When should scope rules be applied to such a subroutine: when the reference is first created, or when the routine is finally called?

动态作用域常使用 shallow binding:

This late binding of the referencing environment of a subroutine that has been passed as a parameter is known as shallow binding.

静态作用域常使用 deep binding:

It therefore makes sense to bind the environment at the time the routine is first passed as a parameter, and then restore that environment when the routine is finally called.

This early binding of the referencing environment is known as deep binding.

Subroutine Closures

Deep binding is implemented by creating an explicit representation of a referencing environment (generally the one in which the subroutine would execute if called at the present time) and bundling it together with a reference to the subroutine. The bundle as a whole is referred to as a closure.

Object Closures

An object that plays the role of a function and its referencing environment may variously be called an object closure, a function object, or a functor.

Macro Expansion

Prior to the development of high-level programming languages, assembly language programmers could find themselves writing highly repetitive code. To ease the burden, many assemblers provided sophisticated macro expansion facilities.

So-called hygienic macros(卫生宏) implicitly encapsulate their arguments, avoiding unexpected interactions with associativity and precedence.

Summary and Concluding Remarks

这一章讨论了名称的主题,以及名称与对象的绑定(在广义上)。我们开始从绑定时间的一般讨论——名称与特定对象关联的时间,或更一般地说,任何开放问题在语言或程序设计或实现中与答案关联的时间。我们定义了对象和名称到对象绑定的生命周期的概念,并指出它们不必相同。然后,我们介绍了三种主要的存储分配机制——静态、栈、和堆——用于管理对象的空间。

在3.3节中,我们描述了名称与对象的绑定是如何受作用域规则的约束。在一些语言中,作用域规则是动态的:一个名称的含义是在最近进入的包含声明且尚未退出的作用域中找到的。然而,在大多数现代语言中,作用域规则是静态的,或者说是词法的:一个名称的含义是在最近的包含声明的词法环绕作用域中找到的。我们发现,词法作用域规则在不同语言之间以重要但有时是微妙的方式变化。我们考虑了哪些类型的作用域是允许嵌套的,作用域是开放的还是封闭的,一个名称的作用域是否包括其声明的整个块,以及是否必须在使用名称之前声明它。我们在3.4节探索了作用域规则的实现。

在3.5节中,我们检查了绑定之间关系的几种方式。别名产生于当在给定作用域中两个或更多名称绑定到同一个对象时。重载产生于一个名称绑定到多个对象时。我们注意到,尽管有时可以通过强制转换或多态性实现类似重载的行为,但底层机制实际上是非常不同的。在3.6节中,我们考虑了何时将引用环境绑定到作为参数传递、从函数返回或存储在变量中的子程序的问题。我们的讨论涉及了闭包和lambda表达式的概念,这两者在后续章节中都会反复出现。在3.7节和3.8节中,我们考虑了宏和分离编译。

词法作用域的一些更复杂的方面说明了对数据抽象的语言支持的发展,这是我们将在第10章回顾的主题。我们首先描述了像Fortran、Algol 60和C这样的语言中的own或静态变量,这些变量允许子程序中的局部变量在一次调用到下一次调用时保持其值。然后我们注意到,简单模块可以被看作是一种使长期存在的对象对一组子程序局部化的方式,这样它们对程序的其他部分来说是不可见的。通过选择性地导出名称,一个模块可以作为一个或多个抽象数据类型的“管理者”。在更高一层的复杂性中,我们注意到有些语言将模块视为类型,允许程序员创建由模块定义的抽象的任意数量的实例。最后,我们注意到面向对象语言通过提供一个继承机制,这个机制允许定义新的抽象(类)作为现有类的扩展或精化,从而扩展了模块作为类型的方法(以及词法作用域的概念)。

在本章考虑的主题中,我们看到了一些有用的特性的例子(递归、静态作用域、前向引用、一级子程序、无限范围),这些特性因为担心实现的复杂性或运行时成本而被某些语言省略。我们还看到了一个特性的例子(模块规范的私有部分),它是为了方便语言的实现而特别引入的,以及另一个(C语言中的独立编译)其设计显然是为了反映特定的实现。在语言设计的几个额外方面(晚绑定与早绑定、静态与动态作用域、对强制转换和转换的支持、对指针和其他别名的容忍),我们看到实现问题起着重要作用。

在类似的脉络中,看似简单的语言规则可能会有出人意料的含义。例如,在3.3.3节中,我们考虑了整个块作用域与名字必须在使用前声明的要求之间的相互作用。就像Fortran的do循环语法和空白规则(2.2.2节)或Pascal的if…then…else语法(2.3.2节),如果选择不当,作用域规则会使程序分析变得困难,这不仅对编译器如此,对人类同样如此。在未来的章节中,我们将看到几个既令人困惑又难以编译的特性示例。当然,语义的实用性和实现的容易程度并不总是一致的。许多容易编译的特性(例如,goto语句)其价值至少是值得怀疑的。我们还将看到几个非常有用且(概念上)简单的特性,比如垃圾收集(8.5.3节)和统一(7.2.4节,C 7.3.2节和12.2.1节),它们的实现却相当复杂。

因为目前博客的主题在文字排版上总感觉有一些问题,再加上想做一个个人的知识整合网站于是就开始折腾Docusaurus。

创建项目

  1. 新建一个GitHub仓库,使用codespace打开。
  2. 在命令行中初始化docusaurus
    1
    2
    npx create-docusaurus@latest my-knowledge-repo classic --typescript
    npm run start
  3. 将文件移动到当前目录:mv my-knowledge-repo/* .
  4. 提交变更,初始化完成

Github Action + Github Pages

根据官方文档Triggering deployment with GitHub Actions操作。

docusaurus.config.ts

除了一些网站的基本描述配置之外还有一些值得注意的配置项:

上方navebar项目配置:Navbar items

代码高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
prism: {
additionalLanguages: [
'java',
'latex',
'haskell',
'matlab',
'PHp',
'bash',
'diff',
'json',
'scss',
],
}

使用官方文档作为例子:

sidebars.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const sidebars: SidebarsConfig = {
technical: [
'technical/introduction',
{
label: 'Programming Language Pragmatics',
type: 'category',
link: {
type: 'generated-index',
title: 'Programming Language Pragmatics',
description:
"Programming Language Pragmatics笔记",
},
items:[
'technical/programming-language-pragmatics/01',
'technical/programming-language-pragmatics/02'
],
}
],
life: [{type: 'autogenerated', dirName: 'life'}],
};
docusaurus.config.ts
1
2
3
4
5
6
items: [
{
label: 'Technical',
to: 'docs/technical',
},
]

MDX语法

警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
:::note

Some **content** with _Markdown_ `syntax`. Check [this `api`](#).

:::

:::tip

Some **content** with _Markdown_ `syntax`. Check [this `api`](#).

:::

:::info

Some **content** with _Markdown_ `syntax`. Check [this `api`](#).

:::

:::warning

Some **content** with _Markdown_ `syntax`. Check [this `api`](#).

:::

:::danger

Some **content** with _Markdown_ `syntax`. Check [this `api`](#).

:::

Scanning

任何编译器或解释器的第一步都是扫描。扫描器将原始源代码作为一系列字符输入,并将其分组为一系列我们称之为标记的块。这些是构成语言语法的有意义的“单词”和“标点符号”。

这一阶段的重点是把字符数组转换成token序列,通过“双指针”的方式逐步消耗字符。

Token类:

1
2
3
4
5
6
class Token {
final TokenType type;
final String lexeme;
final Object literal;
final int line;
}

TokenType:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum TokenType {
// Single-character tokens.
LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE,
COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR,

// One or two character tokens.
BANG, BANG_EQUAL,
EQUAL, EQUAL_EQUAL,
GREATER, GREATER_EQUAL,
LESS, LESS_EQUAL,

// Literals.
IDENTIFIER, STRING, NUMBER,

// Keywords.
AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR,
PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,

EOF
}

处理方式:

1
2
3
4
5
6
7
8
9
private void scanToken() {
char c = advance();
switch (c) {
case '(': addToken(LEFT_PAREN); break;
case '!':
addToken(match('=') ? BANG_EQUAL : BANG);
break;
}
}

Representing Code

在上一章中,我们将原始源代码作为字符串,并将其转换为稍微更高级的表示形式:一系列标记。我们将在下一章中编写的解析器将获取这些标记,并再次将它们转换为更丰富、更复杂的表示形式。

在我们能够生成该表示之前,我们需要定义它。

我们需要一把更大的锤子,而那把锤子就是上下文无关文法(CFG)。它是形式文法工具箱中的下一个最重要的工具。形式文法使用一组称为“字母表”的原子片段。然后它定义了一组通常是无限的“字符串”,这些字符串“属于”文法。每个字符串都是字母表中的“字母”序列。

语言语法的BNF表示:

1
2
3
4
5
6
7
8
9
10
11
expression     → literal
| unary
| binary
| grouping ;

literal → NUMBER | STRING | "true" | "false" | "nil" ;
grouping → "(" expression ")" ;
unary → ( "-" | "!" ) expression ;
binary → expression operator expression ;
operator → "==" | "!=" | "<" | "<=" | ">" | ">="
| "+" | "-" | "*" | "/" ;

Parsing Expressions

在解析之前除了语法规则还需要确定关键字的运算优先级和结合性(左结合、右结合)。

将优先级加入语法规则之后变成下面的形式:

1
2
3
4
5
6
7
8
9
expression     → equality ;
equality → comparison ( ( "!=" | "==" ) comparison )* ;
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
term → factor ( ( "-" | "+" ) factor )* ;
factor → unary ( ( "/" | "*" ) unary )* ;
unary → ( "!" | "-" ) unary
| primary ;
primary → NUMBER | STRING | "true" | "false" | "nil"
| "(" expression ")" ;

Recursive Descent Parsing

递归下降核心步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  private Stmt declaration() {
try {
//> Classes match-class
if (match(CLASS)) return classDeclaration();
//< Classes match-class
//> Functions match-fun
if (match(FUN)) return function("function");
//< Functions match-fun
if (match(VAR)) return varDeclaration();

return statement();
} catch (ParseError error) {
synchronize();
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  private Stmt classDeclaration() {
Token name = consume(IDENTIFIER, "Expect class name.");
//> Inheritance parse-superclass

Expr.Variable superclass = null;
if (match(LESS)) {
consume(IDENTIFIER, "Expect superclass name.");
superclass = new Expr.Variable(previous());
}

//< Inheritance parse-superclass
consume(LEFT_BRACE, "Expect '{' before class body.");

List<Stmt.Function> methods = new ArrayList<>();
while (!check(RIGHT_BRACE) && !isAtEnd()) {
methods.add(function("method"));
}

consume(RIGHT_BRACE, "Expect '}' after class body.");

/* Classes parse-class-declaration < Inheritance construct-class-ast
return new Stmt.Class(name, methods);
*/
//> Inheritance construct-class-ast
return new Stmt.Class(name, superclass, methods);
//< Inheritance construct-class-ast
}

解析的结果是返回一个语句列表:

1
List<Stmt> statements = parser.parse();

stmt类是一个抽象类,被各个具体的语法声明实现,class的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class Class extends Stmt {
Class(Token name,
Expr.Variable superclass,
List<Stmt.Function> methods) {
this.name = name;
this.superclass = superclass;
this.methods = methods;
}

@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitClassStmt(this);
}

final Token name;
final Expr.Variable superclass;
final List<Stmt.Function> methods;
}

Evaluating Expressions

语言实现有各种方式让计算机执行用户源代码的命令。它们可以将其编译成机器码,翻译成另一种高级语言,或将其简化为某种字节码格式,以便虚拟机运行。然而,对于我们的第一个解释器,我们将选择最简单、最直接的路径,直接执行语法树。

使用访问者模式来遍历上面产生的stmt列表直接进行求值,一个操作符遍历的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Object visitBinaryExpr(Expr.Binary expr) {
Object left = evaluate(expr.left);
Object right = evaluate(expr.right);

switch (expr.operator.type) {
case MINUS:
return (double)left - (double)right;
case SLASH:
return (double)left / (double)right;
case STAR:
return (double)left * (double)right;
}

// Unreachable.
return null;
}

Statements and State

为了支持绑定,我们的解释器需要内部状态。当你在程序开头定义一个变量并在结尾处使用它时,解释器必须在此期间保留该变量的值。因此,在本章中,我们将赋予我们的解释器一个不仅能处理,还能记住的大脑。

状态和语句是相辅相成的。由于语句根据定义不会评估为一个值,它们需要做一些其他的事情才能发挥作用。这个事情被称为副作用。它可能意味着产生用户可见的输出或修改解释器中的一些状态,以便以后可以检测到。后者使它们非常适合定义变量或其他命名实体。

Environments

将变量与值关联的绑定需要存储在某个地方。自从Lisp的发明者发明了括号以来,这种数据结构就被称为环境。

1
2
3
class Environment {
private final Map<String, Object> values = new HashMap<>();
}

Scope

作用域定义了一个区域,其中一个名称映射到某个实体。多个作用域使得同一个名称可以在不同的上下文中指代不同的事物。

词法作用域(或较少听到的静态作用域)是一种特定的作用域风格,程序文本本身显示了作用域的开始和结束位置。

通过对求值环境增加嵌套来实现作用域的效果:

1
2
3
4
class Environment {
final Environment enclosing; // 求值环境嵌套
private final Map<String, Object> values = new HashMap<>();
}

Control Flow

目前,我们的解释器只不过是一个计算器。Lox程序只能在完成之前做固定数量的工作。要使其运行时间加倍,必须使源代码长度加倍。我们即将解决这个问题。在本章中,我们的解释器迈出了向编程语言主要联赛迈进的重要一步:图灵完备性。

IF

1
2
3
4
5
6
7
8
9
10
11
12
13
private Stmt ifStatement() {
consume(LEFT_PAREN, "Expect '(' after 'if'.");
Expr condition = expression();
consume(RIGHT_PAREN, "Expect ')' after if condition.");

Stmt thenBranch = statement();
Stmt elseBranch = null;
if (match(ELSE)) {
elseBranch = statement();
}

return new Stmt.If(condition, thenBranch, elseBranch);
}

如何对IF求值:

1
2
3
4
5
6
7
8
9
@Override
public Void visitIfStmt(Stmt.If stmt) {
if (isTruthy(evaluate(stmt.condition))) {
execute(stmt.thenBranch);
} else if (stmt.elseBranch != null) {
execute(stmt.elseBranch);
}
return null;
}

Functions

一旦我们准备好被调用者和参数,剩下的就是执行调用。我们通过将被调用者转换为LoxCallable,然后在其上调用一个 call() 方法来实现这一点。任何可以像函数一样被调用的Lox对象的Java表示都将实现这个接口。这包括自定义函数,当然也包括类对象,因为类被“调用”来构造新实例。

1
2
3
interface LoxCallable {
Object call(Interpreter interpreter, List<Object> arguments);
}

Function Objects

这基本上就是 Stmt.Function 类的作用。我们能不能直接使用它?几乎可以,但还不够。我们还需要一个实现 LoxCallable 接口的类,这样我们才能调用它。我们不希望解释器的运行时阶段渗入前端的语法类,所以我们不希望 Stmt.Function 本身实现这一点。相反,我们将其包装在一个新的类中。

1
2
3
4
5
6
class LoxFunction implements LoxCallable {
private final Stmt.Function declaration;
LoxFunction(Stmt.Function declaration) {
this.declaration = declaration;
}
}

call的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Object call(Interpreter interpreter,
List<Object> arguments) {
Environment environment = new Environment(interpreter.globals);
for (int i = 0; i < declaration.params.size(); i++) {
environment.define(declaration.params.get(i).lexeme,
arguments.get(i));
}

interpreter.executeBlock(declaration.body, environment);
return null;
}

我们在每次调用时创建一个新的环境,而不是在函数声明时。我们之前看到的方法就是这样做的。在调用开始时,它创建一个新的环境。然后它以步调一致地遍历参数和参数列表。对于每一对,它都会使用参数的名称创建一个新的变量,并将其绑定到参数的值。

Classes

类的解析方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
private Stmt classDeclaration() {
Token name = consume(IDENTIFIER, "Expect class name.");
consume(LEFT_BRACE, "Expect '{' before class body.");

List<Stmt.Function> methods = new ArrayList<>();
while (!check(RIGHT_BRACE) && !isAtEnd()) {
methods.add(function("method"));
}

consume(RIGHT_BRACE, "Expect '}' after class body.");

return new Stmt.Class(name, methods);
}

Creating Instances

使用callable来实现类的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LoxClass implements LoxCallable {
final String name;
final LoxClass superclass; // 类的继承
private final Map<String, LoxFunction> methods; // 类方法

@Override
public Object call(Interpreter interpreter,
List<Object> arguments) {
LoxInstance instance = new LoxInstance(this);
// 构造器约定为特殊的函数 名为init
LoxFunction initializer = findMethod("init");
if (initializer != null) {
initializer.bind(instance).call(interpreter, arguments);
}
}

类的实例包含:

1
2
3
4
5
6
class LoxInstance {

private LoxClass klass;

private final Map<String, Object> fields = new HashMap<>(); // 类属性
}
0%