10

함수

그리고 그것은 또한 인간의 마음이 작동하는 방식이다. 오래된 아이디어를 새로운 구조로 결합하여 새로운 아이디어가 되고, 이 새로운 아이디어가 다시 조합되어 끝없이 순환하며, 각 언어의 토양인 기본적인 지상의 이미지에서 점점 더 멀어진다.

더글러스 R. 호프스태터, 나는 이상한 고리다

이 장은 많은 노력의 정점을 이룹니다. 이전 장들은 그 자체로 유용한 기능을 추가했지만, 각각은 퍼즐 조각을 제공했습니다. 우리는 그 조각들(표현식, 문장, 변수, 제어 흐름, 렉시컬 스코프)을 가져와 몇 가지를 더 추가하고, 이 모든 것을 모아 실제 사용자 정의 함수와 함수 호출을 지원할 것입니다.

10 . 1함수 호출

C-스타일 함수 호출 문법에 익숙하시겠지만, 문법은 생각보다 미묘합니다. 호출은 일반적으로 다음과 같은 이름 있는 함수에 사용됩니다.

average(1, 2);

하지만 호출되는 함수의 이름은 실제로는 호출 문법의 일부가 아닙니다. 호출되는 대상(callee)은 함수로 평가되는 모든 표현식이 될 수 있습니다. (물론, 우선순위가 상당히 높은 표현식이어야 하지만, 괄호가 이 문제를 해결해 줍니다.) 예를 들어:

getCallback()();

여기에는 두 개의 호출 표현식이 있습니다. 첫 번째 괄호 쌍은 `getCallback`을 호출 대상으로 가집니다. 하지만 두 번째 호출은 전체 `getCallback()` 표현식을 호출 대상으로 가집니다. 표현식 뒤에 오는 괄호가 함수 호출을 나타냅니다. 호출을 `(`로 시작하는 일종의 후위 연산자처럼 생각할 수 있습니다.

이 "연산자"는 단항 연산자를 포함한 다른 모든 연산자보다 우선순위가 높습니다. 그래서 우리는 `unary` 규칙이 새로운 `call` 규칙으로 올라오도록 하여 문법에 삽입합니다.

unary          → ( "!" | "-" ) unary | call ;
callprimary ( "(" arguments? ")" )* ;

이 규칙은 기본 표현식(primary expression) 뒤에 0개 이상의 함수 호출이 오는 것을 매치합니다. 괄호가 없으면, 이 규칙은 일반적인 기본 표현식을 파싱합니다. 그렇지 않으면, 각 호출은 괄호 쌍과 그 안에 선택적인 인자 목록으로 인식됩니다. 인자 목록 문법은 다음과 같습니다.

argumentsexpression ( "," expression )* ;

이 규칙은 적어도 하나의 인자 표현식이 필요하며, 그 뒤에 쉼표로 구분된 0개 이상의 다른 표현식들이 올 수 있습니다. 0개 인자 호출을 처리하기 위해, `call` 규칙 자체는 전체 `arguments` 프로덕션을 선택 사항으로 간주합니다.

이것은 매우 흔한 "쉼표로 구분된 0개 이상의 요소들" 패턴에 대해 예상했던 것보다 문법적으로 더 어색해 보인다는 것을 인정합니다. 이를 더 잘 처리하는 정교한 메타문법도 있지만, 우리의 BNF와 제가 본 많은 언어 명세에서는 이렇게 번거롭습니다.

구문 트리 생성기에서, 우리는 새로운 노드를 추가합니다.

      "Binary   : Expr left, Token operator, Expr right",
tool/GenerateAst.java
in main()
      "Call     : Expr callee, Token paren, List<Expr> arguments",
      "Grouping : Expr expression",
tool/GenerateAst.java, in main()

이 노드는 호출 대상 표현식(callee expression)과 인자 표현식 목록을 저장합니다. 또한 닫는 괄호에 대한 토큰도 저장합니다. 함수 호출로 인한 런타임 오류를 보고할 때 이 토큰의 위치를 사용할 것입니다.

파서(parser)를 열어봅시다. `unary()`가 이전에 `primary()`를 직접 호출하던 곳을 `call()`을 호출하도록 변경합니다.

      return new Expr.Unary(operator, right);
    }

lox/Parser.java
in unary()
replace 1 line
    return call();
  }
lox/Parser.java, in unary(), replace 1 line

그 정의는 다음과 같습니다.

lox/Parser.java
add after unary()
  private Expr call() {
    Expr expr = primary();

    while (true) { 
      if (match(LEFT_PAREN)) {
        expr = finishCall(expr);
      } else {
        break;
      }
    }

    return expr;
  }
lox/Parser.java, add after unary()

여기 코드는 문법 규칙과 완벽하게 일치하지는 않습니다. 코드 가독성을 높이기 위해 몇 가지를 변경했습니다. 이는 직접 작성하는 파서의 장점 중 하나입니다. 하지만 중위 연산자를 파싱하는 방식과 대략 비슷합니다. 먼저, 호출의 "왼쪽 피연산자"인 기본 표현식을 파싱합니다. 그런 다음, `(`를 볼 때마다 `finishCall()`을 호출하여 이전에 파싱된 표현식을 호출 대상(callee)으로 사용하여 호출 표현식을 파싱합니다. 반환된 표현식은 새로운 `expr`이 되고, 그 결과가 다시 호출되는지 확인하기 위해 루프를 돌게 됩니다.

인자 목록을 파싱하는 코드는 이 헬퍼(helper) 함수에 있습니다.

lox/Parser.java
add after unary()
  private Expr finishCall(Expr callee) {
    List<Expr> arguments = new ArrayList<>();
    if (!check(RIGHT_PAREN)) {
      do {
        arguments.add(expression());
      } while (match(COMMA));
    }

    Token paren = consume(RIGHT_PAREN,
                          "Expect ')' after arguments.");

    return new Expr.Call(callee, paren, arguments);
  }
lox/Parser.java, add after unary()

이것은 `arguments` 문법 규칙을 코드로 거의 그대로 옮긴 것입니다. 단, 0개의 인자를 받는 경우도 처리합니다. 다음 토큰이 `)`인지 확인하여 이 경우를 먼저 확인합니다. 만약 그렇다면, 인자를 파싱하려 하지 않습니다.

그렇지 않으면 표현식을 파싱한 다음, 그 뒤에 다른 인자가 있음을 나타내는 쉼표를 찾습니다. 각 표현식 뒤에 쉼표가 있는 한 이 작업을 계속합니다. 쉼표를 찾지 못하면 인자 목록이 끝난 것이므로 예상되는 닫는 괄호를 소비합니다. 마지막으로, 호출 대상과 해당 인자들을 호출(Call) AST 노드로 묶습니다.

10 . 1 . 1최대 인자 수

현재, 인자를 파싱하는 루프에는 제한이 없습니다. 함수를 호출하고 백만 개의 인자를 전달하고 싶다면, 파서는 아무 문제 없이 처리할 것입니다. 과연 이를 제한해야 할까요?

다른 언어들은 다양한 접근 방식을 취합니다. C 표준은 적합한 구현이 함수에 *적어도* 127개의 인자를 지원해야 한다고 명시하지만, 상한선은 따로 명시하지 않습니다. 자바 명세는 메서드가 *최대* 255개의 인자를 받을 수 있다고 명시합니다.

Lox용 자바 인터프리터는 사실 제한이 필요하지 않지만, 최대 인자 수를 가지는 것은 3부에서 바이트코드 인터프리터를 단순화할 것입니다. 우리는 이처럼 특이한 경우에도 두 인터프리터가 서로 호환되기를 원하므로, 동일한 제한을 jlox에도 추가할 것입니다.

      do {
lox/Parser.java
in finishCall()
        if (arguments.size() >= 255) {
          error(peek(), "Can't have more than 255 arguments.");
        }
        arguments.add(expression());
lox/Parser.java, in finishCall()

여기 코드는 너무 많은 인자를 만나면 오류를 *보고하지만*, 오류를 *던지지는(throw)* 않습니다. 예외를 던지는 것은 파서가 혼란스러운 상태에 있고 문법의 어느 부분에 있는지 더 이상 알 수 없을 때 원하는 패닉 모드로 진입하는 방식입니다. 하지만 여기서는 파서가 여전히 완벽하게 유효한 상태입니다. 단지 너무 많은 인자를 찾았을 뿐입니다. 그래서 오류를 보고하고 계속 진행합니다.

10 . 1 . 2함수 호출 해석

아직 호출할 함수가 없으므로 호출을 먼저 구현하는 것이 이상하게 보일 수 있지만, 그 부분은 나중에 걱정합시다. 먼저, 우리 인터프리터에 새로운 임포트가 필요합니다.

lox/Interpreter.java
import java.util.ArrayList;
import java.util.List;
lox/Interpreter.java

항상 그렇듯이, 해석은 새로운 호출 표현식 노드를 위한 새로운 visit 메서드에서 시작됩니다.

lox/Interpreter.java
add after visitBinaryExpr()
  @Override
  public Object visitCallExpr(Expr.Call expr) {
    Object callee = evaluate(expr.callee);

    List<Object> arguments = new ArrayList<>();
    for (Expr argument : expr.arguments) { 
      arguments.add(evaluate(argument));
    }

    LoxCallable function = (LoxCallable)callee;
    return function.call(this, arguments);
  }
lox/Interpreter.java, add after visitBinaryExpr()

먼저, 호출 대상(callee)에 대한 표현식을 평가합니다. 일반적으로 이 표현식은 함수의 이름을 찾는 식별자에 불과하지만, 무엇이든 될 수 있습니다. 그런 다음 각 인자 표현식을 순서대로 평가하고 결과 값을 목록에 저장합니다.

호출 대상과 인자가 준비되면, 남은 것은 호출을 수행하는 것입니다. 우리는 호출 대상을 LoxCallable로 캐스팅한 다음, 그 위에 `call()` 메서드를 호출하여 이를 수행합니다. 함수처럼 호출될 수 있는 모든 Lox 객체의 자바 표현은 이 인터페이스를 구현할 것입니다. 여기에는 사용자 정의 함수는 물론, 클래스 객체도 포함됩니다. 클래스는 새로운 인스턴스를 생성하기 위해 "호출"되기 때문입니다. 잠시 후 다른 목적을 위해서도 이것을 사용할 것입니다.

이 새로운 인터페이스는 크게 복잡하지 않습니다.

lox/LoxCallable.java
create new file
package com.craftinginterpreters.lox;

import java.util.List;

interface LoxCallable {
  Object call(Interpreter interpreter, List<Object> arguments);
}
lox/LoxCallable.java, create new file

`call()`을 구현하는 클래스에 인터프리터가 필요한 경우를 대비해 인터프리터를 전달합니다. 또한 평가된 인자 값 목록도 함께 전달합니다. 구현자의 임무는 호출 표현식이 생성하는 값을 반환하는 것입니다.

10 . 1 . 3호출 타입 오류

LoxCallable을 구현하기 전에, visit 메서드를 좀 더 견고하게 만들어야 합니다. 현재는 발생하지 않을 것이라고 가정할 수 없는 몇 가지 실패 모드를 무시하고 있습니다. 첫째, 호출 대상이 실제로 호출할 수 있는 것이 아니라면 어떻게 될까요? 다음과 같이 시도하면 어떻게 될까요?

"totally not a function"();

문자열은 Lox에서 호출할 수 없습니다. Lox 문자열의 런타임 표현은 자바 문자열이므로, 이것을 LoxCallable로 캐스팅하면 JVM은 ClassCastException을 발생시킬 것입니다. 우리는 인터프리터가 끔찍한 자바 스택 트레이스를 뱉어내고 죽는 것을 원치 않습니다. 대신, 먼저 타입을 직접 확인해야 합니다.

    }

lox/Interpreter.java
in visitCallExpr()
    if (!(callee instanceof LoxCallable)) {
      throw new RuntimeError(expr.paren,
          "Can only call functions and classes.");
    }

    LoxCallable function = (LoxCallable)callee;
lox/Interpreter.java, in visitCallExpr()

여전히 예외를 던지지만, 이제는 우리만의 예외 타입을 던지며, 인터프리터는 이를 잡아서 우아하게 보고하는 방법을 알고 있습니다.

10 . 1 . 4인자 개수(arity) 확인

다른 문제는 함수의 **arity**와 관련이 있습니다. Arity는 함수나 연산이 기대하는 인자의 수를 나타내는 전문 용어입니다. 단항 연산자는 arity가 1이고, 이항 연산자는 2 등입니다. 함수의 경우 arity는 선언하는 매개변수의 수에 따라 결정됩니다.

fun add(a, b, c) {
  print a + b + c;
}

이 함수는 `a`, `b`, `c` 세 개의 매개변수를 정의하므로 arity는 3이며, 세 개의 인자를 기대합니다. 그렇다면 다음과 같이 호출하면 어떻게 될까요?

add(1, 2, 3, 4); // 너무 많음.
add(1, 2);       // 너무 적음.

다른 언어들은 이 문제에 대해 다른 접근 방식을 취합니다. 물론 대부분의 정적 타입 언어는 컴파일 시간에 이를 확인하고, 인자 수가 함수의 arity와 일치하지 않으면 코드를 컴파일하지 않습니다. JavaScript는 전달하는 초과 인자를 무시합니다. 충분한 인자를 전달하지 않으면, 누락된 매개변수를 마법 같은, `null`과 비슷하지만 실제로는 다른 `undefined` 값으로 채웁니다. Python은 더 엄격합니다. 인자 목록이 너무 짧거나 너무 길면 런타임 오류를 발생시킵니다.

저는 후자의 접근 방식이 더 좋다고 생각합니다. 잘못된 수의 인자를 전달하는 것은 거의 항상 버그이며, 제가 실제로 저지르는 실수입니다. 그렇다면 구현체가 빨리 알려줄수록 좋습니다. 그래서 Lox에서는 Python의 접근 방식을 따를 것입니다. 호출 가능 객체를 호출하기 전에, 인자 목록의 길이가 호출 가능 객체의 arity와 일치하는지 확인합니다.

    LoxCallable function = (LoxCallable)callee;
lox/Interpreter.java
in visitCallExpr()
    if (arguments.size() != function.arity()) {
      throw new RuntimeError(expr.paren, "Expected " +
          function.arity() + " arguments but got " +
          arguments.size() + ".");
    }

    return function.call(this, arguments);
lox/Interpreter.java, in visitCallExpr()

이것은 LoxCallable 인터페이스에 arity를 묻는 새로운 메서드가 필요합니다.

interface LoxCallable {
lox/LoxCallable.java
in interface LoxCallable
  int arity();
  Object call(Interpreter interpreter, List<Object> arguments);
lox/LoxCallable.java, in interface LoxCallable

arity 검사를 `call()`의 구체적인 구현으로 밀어 넣을 수도 있습니다. 하지만 LoxCallable을 구현하는 여러 클래스가 있을 것이므로, 이는 몇몇 클래스에 걸쳐 중복된 유효성 검사로 이어질 것입니다. 이를 visit 메서드로 끌어올리면 한 곳에서 처리할 수 있습니다.

10 . 2네이티브 함수

이론적으로는 함수를 호출할 수 있지만, 아직 호출할 함수가 없습니다. 사용자 정의 함수에 도달하기 전에, 언어 구현의 필수적이지만 종종 간과되는 측면인 **네이티브 함수(native functions)**를 소개하기에 좋은 시점입니다. 이 함수들은 인터프리터가 사용자 코드에 노출하지만, 구현 중인 언어(Lox)가 아닌 호스트 언어(여기서는 자바)로 구현됩니다.

때로는 **프리미티브(primitives)**, **외부 함수(external functions)** 또는 **외래 함수(foreign functions)**라고 불리기도 합니다. 이 함수들은 사용자 프로그램이 실행되는 동안 호출될 수 있으므로, 구현체의 런타임의 일부를 구성합니다. 많은 프로그래밍 언어 책들이 이러한 함수들을 개념적으로 흥미롭지 않다고 여겨 간과합니다. 대부분 단순 작업에 가깝습니다.

하지만 언어가 유용한 일을 잘 하도록 만드는 데 있어서는, 구현체가 제공하는 네이티브 함수가 핵심입니다. 이 함수들은 모든 프로그램이 정의되는 근본적인 서비스에 대한 접근을 제공합니다. 파일 시스템에 접근하는 네이티브 함수를 제공하지 않는다면, 사용자는 파일을 읽고 표시하는 프로그램을 작성하는 데 엄청난 어려움을 겪을 것입니다.

많은 언어는 사용자에게 자신만의 네이티브 함수를 제공할 수 있도록 허용하기도 합니다. 이를 위한 메커니즘을 **외래 함수 인터페이스(foreign function interface, FFI)**, **네이티브 확장(native extension)**, **네이티브 인터페이스(native interface)** 등으로 부릅니다. 이러한 기능은 언어 구현자가 기본 플랫폼이 지원하는 모든 기능에 대한 접근을 제공할 필요가 없게 해주기 때문에 좋습니다. 우리는 jlox를 위한 FFI를 정의하지는 않겠지만, 어떤 모습인지 감을 잡을 수 있도록 하나의 네이티브 함수를 추가할 것입니다.

10 . 2 . 1시간 알려주기

3부에 도달하여 Lox의 훨씬 더 효율적인 구현을 시작하면 성능에 깊이 관심을 가질 것입니다. 성능 작업에는 측정이 필요하며, 이는 **벤치마크(benchmarks)**를 의미합니다. 벤치마크는 인터프리터의 특정 부분을 실행하는 데 걸리는 시간을 측정하는 프로그램입니다.

인터프리터 시작, 벤치마크 실행, 종료까지 걸리는 시간을 측정할 수 있지만, 이는 JVM 시작 시간, OS 관련 문제 등 많은 오버헤드를 추가합니다. 물론 이러한 것들이 중요하지만, 단순히 인터프리터의 특정 부분에 대한 최적화를 검증하려는 경우, 이러한 오버헤드가 결과에 영향을 미치는 것을 원치 않을 것입니다.

더 좋은 해결책은 벤치마크 스크립트 자체가 코드 내 두 지점 사이의 경과 시간을 측정하는 것입니다. 이를 위해 Lox 프로그램은 시간을 알 수 있어야 합니다. 현재는 그렇게 할 방법이 없습니다. 컴퓨터의 기본 클록에 접근할 수 없다면 유용한 클록을 "맨 처음부터" 구현할 수 없습니다.

그래서 우리는 특정 시점 이후 경과된 초를 반환하는 네이티브 함수인 `clock()`을 추가할 것입니다. 두 번의 연속 호출 사이의 차이는 두 호출 사이에 얼마나 많은 시간이 경과했는지를 알려줍니다. 이 함수는 전역 스코프에 정의되므로, 인터프리터가 이에 접근할 수 있도록 합시다.

class Interpreter implements Expr.Visitor<Object>,
                             Stmt.Visitor<Void> {
lox/Interpreter.java
in class Interpreter
replace 1 line
  final Environment globals = new Environment();
  private Environment environment = globals;

  void interpret(List<Stmt> statements) {
lox/Interpreter.java, in class Interpreter, replace 1 line

인터프리터의 `environment` 필드는 지역 스코프에 진입하고 나갈 때 변경됩니다. 이 필드는 *현재* 환경을 추적합니다. 새로운 `globals` 필드는 가장 바깥쪽 전역 환경에 대한 고정된 참조를 가집니다.

Interpreter를 인스턴스화할 때, 우리는 이 네이티브 함수를 전역 스코프에 넣습니다.

  private Environment environment = globals;

lox/Interpreter.java
in class Interpreter
  Interpreter() {
    globals.define("clock", new LoxCallable() {
      @Override
      public int arity() { return 0; }

      @Override
      public Object call(Interpreter interpreter,
                         List<Object> arguments) {
        return (double)System.currentTimeMillis() / 1000.0;
      }

      @Override
      public String toString() { return "<native fn>"; }
    });
  }

  void interpret(List<Stmt> statements) {
lox/Interpreter.java, in class Interpreter

이 코드는 "clock"이라는 변수를 정의합니다. 이 변수의 값은 LoxCallable을 구현하는 자바 익명 클래스입니다. `clock()` 함수는 인자를 받지 않으므로 arity는 0입니다. `call()` 구현은 해당 자바 함수를 호출하고 결과를 초 단위의 double 값으로 변환합니다.

사용자로부터 입력을 받거나 파일 작업 등 다른 네이티브 함수를 추가하고 싶다면, 각각을 LoxCallable을 구현하는 익명 클래스로 추가할 수 있습니다. 하지만 이 책에서는 이 함수 하나면 충분합니다.

이제 함수를 정의하는 일은 사용자에게 맡기고, 우리는 이만 손을 떼도록 합시다... 

10 . 3함수 선언

드디어 변수를 추가했을 때 도입했던 `declaration` 규칙에 새로운 프로덕션을 추가할 수 있게 되었습니다. 함수 선언은 변수와 마찬가지로 새로운 이름을 바인딩합니다. 즉, 선언이 허용되는 곳에서만 허용됩니다.

declarationfunDecl
               | varDecl
               | statement ;

업데이트된 `declaration` 규칙은 이 새로운 규칙을 참조합니다.

funDecl"fun" function ;
functionIDENTIFIER "(" parameters? ")" block ;

주요 `funDecl` 규칙은 별도의 헬퍼 규칙 `function`을 사용합니다. 함수 *선언문*은 `fun` 키워드 뒤에 실제 함수 관련 내용이 옵니다. 클래스로 넘어가면, 메서드를 선언하기 위해 `function` 규칙을 재사용할 것입니다. 메서드는 함수 선언과 비슷하게 보이지만, `fun` 키워드로 시작하지 않습니다.

함수 자체는 이름 뒤에 괄호로 묶인 매개변수 목록과 본문이 옵니다. 본문은 항상 중괄호로 묶인 블록이며, 블록 문장과 동일한 문법 규칙을 사용합니다. 매개변수 목록은 이 규칙을 사용합니다.

parametersIDENTIFIER ( "," IDENTIFIER )* ;

이는 이전에 본 `arguments` 규칙과 비슷하지만, 각 매개변수가 표현식이 아니라 식별자라는 점이 다릅니다. 파서가 처리해야 할 새로운 문법이 많지만, 결과적인 AST 노드는 그리 나쁘지 않습니다.

      "Expression : Expr expression",
tool/GenerateAst.java
in main()
      "Function   : Token name, List<Token> params," +
                  " List<Stmt> body",
      "If         : Expr condition, Stmt thenBranch," +
tool/GenerateAst.java, in main()

함수 노드는 이름, 매개변수 목록(이름들), 그리고 본문을 가집니다. 본문은 중괄호 안에 포함된 문장 목록으로 저장됩니다.

파서에서, 우리는 새로운 선언을 추가합니다.

    try {
lox/Parser.java
in declaration()
      if (match(FUN)) return function("function");
      if (match(VAR)) return varDeclaration();
lox/Parser.java, in declaration()

다른 문장들과 마찬가지로, 함수는 선행 키워드로 인식됩니다. `fun`을 만나면 `function`을 호출합니다. 이는 우리가 이미 `fun` 키워드를 매치하고 소비했으므로 `function` 문법 규칙에 해당합니다. 이 메서드를 한 번에 하나씩 만들어 나갈 것이며, 다음으로 시작합니다.

lox/Parser.java
add after expressionStatement()
  private Stmt.Function function(String kind) {
    Token name = consume(IDENTIFIER, "Expect " + kind + " name.");
  }
lox/Parser.java, add after expressionStatement()

현재는 함수의 이름에 대한 식별자 토큰만 소비합니다. 그 이상한 `kind` 매개변수가 궁금할 수도 있습니다. 문법 규칙을 재사용하는 것처럼, 나중에 클래스 내부의 메서드를 파싱하기 위해 `function()` 메서드를 재사용할 것입니다. 그렇게 할 때 `kind`에 "method"를 전달하여 오류 메시지가 파싱되는 선언의 종류에 따라 구체화되도록 할 것입니다.

다음으로, 매개변수 목록과 그것을 감싸는 한 쌍의 괄호를 파싱합니다.

    Token name = consume(IDENTIFIER, "Expect " + kind + " name.");
lox/Parser.java
in function()
    consume(LEFT_PAREN, "Expect '(' after " + kind + " name.");
    List<Token> parameters = new ArrayList<>();
    if (!check(RIGHT_PAREN)) {
      do {
        if (parameters.size() >= 255) {
          error(peek(), "Can't have more than 255 parameters.");
        }

        parameters.add(
            consume(IDENTIFIER, "Expect parameter name."));
      } while (match(COMMA));
    }
    consume(RIGHT_PAREN, "Expect ')' after parameters.");
  }
lox/Parser.java, in function()

이것은 호출에서 인자를 처리하는 코드와 비슷하지만, 헬퍼 메서드로 분리되지 않았습니다. 외부 `if` 문은 0개의 매개변수 케이스를 처리하고, 내부 `while` 루프는 매개변수를 구분하는 쉼표를 찾을 때까지 매개변수를 파싱합니다. 결과는 각 매개변수 이름에 대한 토큰 목록입니다.

함수 호출의 인자와 마찬가지로, 함수가 가질 수 있는 최대 매개변수 수를 초과하지 않도록 파싱 시점에 유효성 검사를 수행합니다.

마지막으로, 본문을 파싱하고 이 모든 것을 함수 노드로 묶습니다.

    consume(RIGHT_PAREN, "Expect ')' after parameters.");
lox/Parser.java
in function()

    consume(LEFT_BRACE, "Expect '{' before " + kind + " body.");
    List<Stmt> body = block();
    return new Stmt.Function(name, parameters, body);
  }
lox/Parser.java, in function()

여기서 `block()`을 호출하기 전에 본문의 시작 부분에 있는 `{`를 소비한다는 점에 유의하세요. `block()`은 중괄호 토큰이 이미 매치되었다고 가정하기 때문입니다. 여기서 `{`를 소비하면 `{`가 발견되지 않을 경우 함수 선언의 컨텍스트에서임을 알 수 있으므로 더 정확한 오류 메시지를 보고할 수 있습니다.

10 . 4함수 객체

이제 구문을 파싱했으니 일반적으로 해석할 준비가 되었지만, 먼저 자바에서 Lox 함수를 어떻게 표현할지 생각해 보아야 합니다. 함수가 호출될 때 매개변수를 인자 값에 바인딩할 수 있도록 매개변수를 추적해야 합니다. 그리고 물론, 함수의 본문 코드를 실행할 수 있도록 유지해야 합니다.

이것이 기본적으로 `Stmt.Function` 클래스입니다. 그것을 그냥 사용할 수 있을까요? 거의 그렇지만, 완전히는 아닙니다. 우리는 또한 `LoxCallable`을 구현하는 클래스가 필요합니다. 그래야 호출할 수 있습니다. 인터프리터의 런타임 단계가 프론트엔드의 구문 클래스로 흘러들어가는 것을 원치 않으므로 `Stmt.Function` 자체는 그것을 구현하지 않습니다. 대신, 그것을 새로운 클래스로 래핑합니다.

lox/LoxFunction.java
create new file
package com.craftinginterpreters.lox;

import java.util.List;

class LoxFunction implements LoxCallable {
  private final Stmt.Function declaration;
  LoxFunction(Stmt.Function declaration) {
    this.declaration = declaration;
  }
}
lox/LoxFunction.java, create new file

LoxCallable의 `call()`을 다음과 같이 구현합니다.

lox/LoxFunction.java
add after LoxFunction()
  @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;
  }
lox/LoxFunction.java, add after LoxFunction()

이 몇 줄의 코드는 우리 인터프리터의 가장 근본적이고 강력한 부분 중 하나입니다. 문장과 상태 장에서 보았듯이, 이름 환경을 관리하는 것은 언어 구현의 핵심 부분입니다. 함수는 이와 깊이 연관되어 있습니다.

매개변수는 함수의 핵심이며, 특히 함수가 매개변수를 *캡슐화한다*는 사실이 중요합니다. 함수 외부의 어떤 코드도 매개변수를 볼 수 없습니다. 이는 각 함수가 이러한 변수를 저장하는 자체 환경을 가진다는 것을 의미합니다.

더 나아가, 이 환경은 동적으로 생성되어야 합니다. 각 함수 *호출*은 자체 환경을 가집니다. 그렇지 않으면 재귀가 깨질 것입니다. 같은 함수에 대한 여러 호출이 동시에 실행 중인 경우, 비록 모두 같은 함수에 대한 호출이라도 각각 *자신만의* 환경이 필요합니다.

예를 들어, 3을 세는 복잡한 방법은 다음과 같습니다.

fun count(n) {
  if (n > 1) count(n - 1);
  print n;
}

count(3);

가장 안쪽에 중첩된 호출에서 1을 출력하기 직전에 인터프리터를 일시 중지한다고 상상해 봅시다. 2와 3을 출력하는 외부 호출은 아직 값을 출력하지 않았으므로, 메모리 어딘가에는 여전히 `n`이 한 컨텍스트에서는 3, 다른 컨텍스트에서는 2, 가장 안쪽에서는 1에 바인딩되어 있다는 사실을 저장하고 있는 환경들이 있어야 합니다. 예를 들어:

각 재귀 호출을 위한 별도의 환경.

그렇기 때문에 함수 *선언* 시점이 아닌 각 함수 *호출* 시점에 새로운 환경을 생성합니다. 앞서 보았던 `call()` 메서드가 이 역할을 합니다. 호출이 시작될 때 새로운 환경을 생성합니다. 그런 다음 매개변수와 인자 목록을 동기화하여 순회합니다. 각 쌍에 대해 매개변수의 이름으로 새 변수를 생성하고 인자 값에 바인딩합니다.

따라서 다음과 같은 프로그램의 경우:

fun add(a, b, c) {
  print a + b + c;
}

add(1, 2, 3);

`add()` 호출 시점에 인터프리터는 다음과 같은 것을 생성합니다.

인자를 매개변수에 바인딩.

그런 다음 `call()`은 인터프리터에게 이 새로운 함수-지역 환경에서 함수의 본문을 실행하도록 지시합니다. 지금까지는 현재 환경이 함수가 호출되는 환경이었습니다. 이제 우리는 거기에서 함수를 위해 생성한 새로운 매개변수 공간 내부로 순간 이동합니다.

이것이 데이터를 함수로 전달하는 데 필요한 전부입니다. 본문을 실행할 때 다른 환경을 사용함으로써, 동일한 코드를 가진 동일한 함수에 대한 호출이 다른 결과를 생성할 수 있습니다.

함수의 본문 실행이 완료되면, `executeBlock()`은 해당 함수-지역 환경을 버리고 호출 시점에서 활성화되어 있던 이전 환경을 복원합니다. 마지막으로, `call()`은 `null`을 반환하며, 이는 호출자에게 `nil`로 전달됩니다. (반환 값은 나중에 추가할 것입니다.)

기계적으로는 코드가 상당히 간단합니다. 몇 개의 목록을 순회하고, 새로운 변수를 바인딩하고, 메서드를 호출합니다. 하지만 이곳이 바로 함수의 선언이라는 명확한 *코드*가 살아 숨 쉬는 *호출*로 변하는 지점입니다. 이 책 전체에서 제가 가장 좋아하는 코드 조각 중 하나입니다. 관심이 있다면 잠시 묵상하는 시간을 가져도 좋습니다.

다 되었나요? 좋습니다. 매개변수를 바인딩할 때, 우리는 매개변수와 인자 목록의 길이가 같다고 가정합니다. 이는 `visitCallExpr()`가 `call()`을 호출하기 전에 arity를 확인하므로 안전합니다. 이 확인은 함수가 자신의 arity를 보고하는 것에 의존합니다.

lox/LoxFunction.java
add after LoxFunction()
  @Override
  public int arity() {
    return declaration.params.size();
  }
lox/LoxFunction.java, add after LoxFunction()

이것이 우리 객체 표현의 대부분입니다. 이왕 여기 온 김에 `toString()`도 구현해 봅시다.

lox/LoxFunction.java
add after LoxFunction()
  @Override
  public String toString() {
    return "<fn " + declaration.name.lexeme + ">";
  }
lox/LoxFunction.java, add after LoxFunction()

이것은 사용자가 함수 값을 출력할 때 더 보기 좋은 출력을 제공합니다.

fun add(a, b) {
  print a + b;
}

print add; // "<fn add>".

10 . 4 . 1함수 선언 해석

LoxFunction은 곧 다시 수정하겠지만, 이 정도면 시작하기에 충분합니다. 이제 함수 선언을 방문할 수 있습니다.

lox/Interpreter.java
add after visitExpressionStmt()
  @Override
  public Void visitFunctionStmt(Stmt.Function stmt) {
    LoxFunction function = new LoxFunction(stmt);
    environment.define(stmt.name.lexeme, function);
    return null;
  }
lox/Interpreter.java, add after visitExpressionStmt()

이것은 다른 리터럴 표현식을 해석하는 방식과 유사합니다. 함수 *문법 노드*—함수의 컴파일 타임 표현—를 가져와 런타임 표현으로 변환합니다. 여기서 런타임 표현은 문법 노드를 래핑하는 LoxFunction입니다.

함수 선언은 다른 리터럴 노드와 다르게, 선언이 결과 객체를 새로운 변수에 *바인딩하기도* 합니다. 따라서 LoxFunction을 생성한 후, 현재 환경에 새로운 바인딩을 생성하고 거기에 참조를 저장합니다.

이로써 우리는 Lox 내에서 직접 함수를 정의하고 호출할 수 있습니다. 한번 시도해보세요:

fun sayHi(first, last) {
  print "Hi, " + first + " " + last + "!";
}

sayHi("Dear", "Reader");

여러분은 어떨지 모르겠지만, 저에게는 진정한 프로그래밍 언어처럼 보입니다.

10 . 5Return 문

매개변수를 전달하여 데이터를 함수 안으로 가져올 수는 있지만, 결과를 다시 밖으로 가져올 방법이 없습니다. Lox가 Ruby나 Scheme처럼 표현식 지향 언어였다면, 본문은 암묵적으로 함수의 결과가 되는 표현식이었을 것입니다. 하지만 Lox에서는 함수의 본문이 값을 생성하지 않는 문장 목록이므로, 결과를 반환하기 위한 전용 문법이 필요합니다. 다시 말해, `return` 문입니다. 이미 문법을 짐작할 수 있을 것입니다.

statementexprStmt
               | forStmt
               | ifStmt
               | printStmt
               | returnStmt
               | whileStmt
               | block ;

returnStmt"return" expression? ";" ;

우리는 존경받는 `statement` 규칙 아래에 하나 더, 사실상 마지막 프로덕션을 갖게 되었습니다. `return` 문은 `return` 키워드 뒤에 선택적인 표현식이 오고 세미콜론으로 끝납니다.

반환 값은 유용한 값을 반환하지 않는 함수에서 일찍 종료하는 것을 지원하기 위해 선택 사항입니다. 정적 타입 언어에서는 "void" 함수는 값을 반환하지 않고, non-void 함수는 값을 반환합니다. Lox는 동적 타입이므로 진정한 void 함수는 없습니다. 컴파일러는 `return` 문을 포함하지 않는 함수 호출의 결과 값을 가져오는 것을 막을 방법이 없습니다.

fun procedure() {
  print "don't return anything";
}

var result = procedure();
print result; // ?

이것은 모든 Lox 함수가 `return` 문을 전혀 포함하지 않더라도 *무언가*를 반환해야 한다는 것을 의미합니다. 우리는 이를 위해 `nil`을 사용하며, 이것이 `LoxFunction`의 `call()` 구현이 마지막에 `null`을 반환하는 이유입니다. 같은 맥락에서, `return` 문에서 값을 생략하면 단순히 다음과 동일하게 취급합니다.

return nil;

AST 생성기에서, 우리는 새로운 노드를 추가합니다.

      "Print      : Expr expression",
tool/GenerateAst.java
in main()
      "Return     : Token keyword, Expr value",
      "Var        : Token name, Expr initializer",
tool/GenerateAst.java, in main()

`return` 키워드 토큰을 유지하여 오류 보고 시 해당 위치를 사용할 수 있도록 하며, 반환되는 값(있는 경우)을 저장합니다. 다른 문장과 마찬가지로, 초기 키워드를 인식하는 것으로 파싱을 시작합니다.

    if (match(PRINT)) return printStatement();
lox/Parser.java
in statement()
    if (match(RETURN)) return returnStatement();
    if (match(WHILE)) return whileStatement();
lox/Parser.java, in statement()

그것은 다음으로 분기됩니다:

lox/Parser.java
add after printStatement()
  private Stmt returnStatement() {
    Token keyword = previous();
    Expr value = null;
    if (!check(SEMICOLON)) {
      value = expression();
    }

    consume(SEMICOLON, "Expect ';' after return value.");
    return new Stmt.Return(keyword, value);
  }
lox/Parser.java, add after printStatement()

이전에 소비된 `return` 키워드를 확보한 후, 값 표현식을 찾습니다. 많은 다른 토큰이 표현식을 시작할 수 있으므로, 반환 값이 *있는지* 판단하기 어렵습니다. 대신 *없는 경우*를 확인합니다. 세미콜론은 표현식을 시작할 수 없으므로, 다음 토큰이 세미콜론이라면 값이 없음을 알 수 있습니다.

10 . 5 . 1호출에서 반환하기

`return` 문을 해석하는 것은 까다롭습니다. 함수의 본문 내 어디에서든, 심지어 다른 문장 안에 깊이 중첩되어 있어도 반환할 수 있습니다. `return`이 실행될 때, 인터프리터는 현재 있는 모든 컨텍스트에서 완전히 벗어나 함수 호출이 완료되도록 해야 합니다. 마치 강력한 제어 흐름 구조처럼 말이죠.

예를 들어, 다음과 같은 프로그램을 실행하고 있고 `return` 문을 실행하기 직전이라고 가정해 봅시다.

fun count(n) {
  while (n < 100) {
    if (n == 3) return n; // <--
    print n;
    n = n + 1;
  }
}

count(1);

현재 자바 호출 스택은 대략 다음과 같습니다.

Interpreter.visitReturnStmt()
Interpreter.visitIfStmt()
Interpreter.executeBlock()
Interpreter.visitBlockStmt()
Interpreter.visitWhileStmt()
Interpreter.executeBlock()
LoxFunction.call()
Interpreter.visitCallExpr()

스택의 맨 위에서 `call()`까지 완전히 되돌아가야 합니다. 여러분은 어떨지 모르겠지만, 저에게는 예외처럼 들립니다. `return` 문을 실행할 때, 예외를 사용하여 인터프리터를 포함하는 모든 문장의 visit 메서드를 지나 본문 실행을 시작한 코드로 되감을 것입니다.

새로운 AST 노드를 위한 visit 메서드는 다음과 같습니다.

lox/Interpreter.java
add after visitPrintStmt()
  @Override
  public Void visitReturnStmt(Stmt.Return stmt) {
    Object value = null;
    if (stmt.value != null) value = evaluate(stmt.value);

    throw new Return(value);
  }
lox/Interpreter.java, add after visitPrintStmt()

반환 값이 있으면 평가하고, 그렇지 않으면 `nil`을 사용합니다. 그런 다음 그 값을 사용자 정의 예외 클래스로 래핑하여 던집니다.

lox/Return.java
create new file
package com.craftinginterpreters.lox;

class Return extends RuntimeException {
  final Object value;

  Return(Object value) {
    super(null, null, false, false);
    this.value = value;
  }
}
lox/Return.java, create new file

이 클래스는 자바 런타임 예외 클래스에 필요한 요소를 갖추고 반환 값을 래핑합니다. `null`과 `false` 인자를 사용한 이상한 super 생성자 호출은 우리가 필요 없는 일부 JVM 메커니즘을 비활성화합니다. 우리는 예외 클래스를 실제 오류 처리 목적이 아닌 제어 흐름 목적으로 사용하므로, 스택 트레이스와 같은 오버헤드는 필요 없습니다.

우리는 이것이 함수 호출이 시작된 곳, 즉 LoxFunction의 `call()` 메서드까지 완전히 되감기를 원합니다.

          arguments.get(i));
    }

lox/LoxFunction.java
in call()
replace 1 line
    try {
      interpreter.executeBlock(declaration.body, environment);
    } catch (Return returnValue) {
      return returnValue.value;
    }
    return null;
lox/LoxFunction.java, in call(), replace 1 line

`executeBlock()` 호출을 try-catch 블록으로 감쌉니다. return 예외가 발생하면, 해당 값을 추출하여 `call()`의 반환 값으로 만듭니다. 이러한 예외 중 어느 것도 잡히지 않으면, 함수가 `return` 문을 만나지 않고 본문의 끝에 도달했다는 의미입니다. 이 경우, 암묵적으로 `nil`을 반환합니다.

한번 시험해 봅시다. 우리는 마침내 이 고전적인 예제—피보나치 수를 계산하는 재귀 함수—를 지원할 수 있게 되었습니다.

fun fib(n) {
  if (n <= 1) return n;
  return fib(n - 2) + fib(n - 1);
}

for (var i = 0; i < 20; i = i + 1) {
  print fib(i);
}

이 작은 프로그램은 우리가 지난 몇 챕터 동안 구현하는 데 시간을 보냈던 거의 모든 언어 기능, 즉 표현식, 산술, 분기, 반복, 변수, 함수, 함수 호출, 매개변수 바인딩, 그리고 반환 기능을 사용합니다.

10 . 6지역 함수와 클로저

우리 함수들은 꽤 완전한 기능을 갖추었지만, 메워야 할 한 가지 구멍이 있습니다. 사실, 이 구멍은 너무 커서 다음 장의 대부분을 할애하여 메울 예정이지만, 여기서 시작할 수 있습니다.

LoxFunction의 `call()` 구현은 함수 매개변수를 바인딩하는 새로운 환경을 생성합니다. 그 코드를 보여줄 때, 저는 한 가지 중요한 점을 간과했습니다. 그 환경의 *부모*는 무엇일까요?

현재는 항상 최상위 전역 환경인 `globals`입니다. 이렇게 되면, 식별자가 함수 본문 자체에 정의되어 있지 않을 때 인터프리터는 전역 스코프에서 외부를 찾아 식별자를 찾을 수 있습니다. 피보나치 예제에서는 이것이 인터프리터가 함수 본문 내에서 `fib`의 재귀 호출을 찾을 수 있는 방법입니다. `fib`는 전역 변수이기 때문입니다.

하지만 Lox에서는 함수 선언이 이름이 바인딩될 수 있는 *어디든* 허용된다는 것을 기억하세요. 여기에는 Lox 스크립트의 최상위뿐만 아니라 블록 내부 또는 다른 함수 내부도 포함됩니다. Lox는 다른 함수 내부에 정의되거나 블록 내부에 중첩된 **지역 함수(local functions)**를 지원합니다.

다음 고전적인 예제를 생각해 봅시다.

fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1".
counter(); // "2".

여기서 `count()`는 자신을 포함하는 함수 `makeCounter()` 내부에 선언된 `i`를 사용합니다. `makeCounter()`는 `count()` 함수에 대한 참조를 반환하고, 그 자신의 본문 실행을 완전히 마칩니다.

한편, 최상위 코드는 반환된 `count()` 함수를 호출합니다. 이 호출은 `count()`의 본문을 실행하며, `i`가 정의된 함수가 이미 종료되었음에도 불구하고 `i`에 할당하고 읽습니다.

중첩 함수가 있는 언어를 처음 접한다면 이것이 이상하게 보일 수 있지만, 사용자들은 작동할 것이라고 기대합니다. 아쉽게도, 지금 실행하면 `count()`의 본문이 `i`를 찾으려 할 때 `counter()` 호출에서 정의되지 않은 변수 오류가 발생합니다. 이는 적용되는 환경 체인이 다음과 같기 때문입니다.

count() 본문에서 전역 스코프로 가는 환경 체인.

`count()`를 호출할 때(`counter`에 저장된 참조를 통해), 우리는 함수 본문을 위한 새로운 빈 환경을 생성합니다. 그 부모는 전역 환경입니다. `i`가 바인딩된 `makeCounter()`를 위한 환경을 잃었습니다.

잠시 시간을 거슬러 올라가 봅시다. `makeCounter()`의 본문 내부에 `count()`를 선언했을 때의 환경 체인은 다음과 같았습니다.

makeCounter() 본문 내부의 환경 체인.

따라서 함수가 선언된 시점에는 `i`를 볼 수 있습니다. 하지만 `makeCounter()`에서 반환되어 그 본문을 종료하면 인터프리터는 해당 환경을 버립니다. 인터프리터가 `count()`를 둘러싼 환경을 유지하지 않으므로, 함수 객체 자체가 이를 유지해야 합니다.

이러한 데이터 구조를 **클로저(closure)**라고 부르는데, 함수가 선언된 곳의 주변 변수를 "둘러싸고" 유지하기 때문입니다. 클로저는 초기 Lisp 시절부터 존재했으며, 언어 해커들은 이를 구현하기 위한 온갖 방법을 고안해냈습니다. jlox에서는 작동하는 가장 간단한 방법을 사용할 것입니다. LoxFunction에 환경을 저장할 필드를 추가합니다.

  private final Stmt.Function declaration;
lox/LoxFunction.java
in class LoxFunction
  private final Environment closure;

  LoxFunction(Stmt.Function declaration) {
lox/LoxFunction.java, in class LoxFunction

생성자에서 이를 초기화합니다.

lox/LoxFunction.java
constructor LoxFunction()
replace 1 line
  LoxFunction(Stmt.Function declaration, Environment closure) {
    this.closure = closure;
    this.declaration = declaration;
lox/LoxFunction.java, constructor LoxFunction(), replace 1 line

LoxFunction을 생성할 때 현재 환경을 캡처합니다.

  public Void visitFunctionStmt(Stmt.Function stmt) {
lox/Interpreter.java
in visitFunctionStmt()
replace 1 line
    LoxFunction function = new LoxFunction(stmt, environment);
    environment.define(stmt.name.lexeme, function);
lox/Interpreter.java, in visitFunctionStmt(), replace 1 line

이것은 함수가 *호출*될 때가 아니라 *선언*될 때 활성화되는 환경이며, 이는 우리가 원하는 것입니다. 이는 함수 선언을 둘러싼 렉시컬 스코프를 나타냅니다. 마지막으로, 함수를 호출할 때 `globals`로 바로 가는 대신 그 환경을 호출의 부모로 사용합니다.

                     List<Object> arguments) {
lox/LoxFunction.java
in call()
replace 1 line
    Environment environment = new Environment(closure);
    for (int i = 0; i < declaration.params.size(); i++) {
lox/LoxFunction.java, in call(), replace 1 line

이것은 함수의 본문에서부터 함수가 선언된 환경을 거쳐 전역 스코프까지 이어지는 환경 체인을 생성합니다. 런타임 환경 체인이 우리가 원하는 대로 소스 코드의 텍스트적 중첩과 일치합니다. 이 함수를 호출할 때의 최종 결과는 다음과 같습니다.

클로저가 있는 환경 체인.

이제 보시다시피, 인터프리터는 `i`가 환경 체인의 중간에 있기 때문에 필요할 때 `i`를 여전히 찾을 수 있습니다. 이제 `makeCounter()` 예제를 실행해 보세요. 작동합니다!

함수는 코드를 추상화하고, 재사용하며, 조합할 수 있게 해줍니다. Lox는 예전의 초보적인 산술 계산기보다 훨씬 강력해졌습니다. 아쉽게도, 클로저를 급하게 끼워 넣으면서 다이내믹 스코핑이 아주 조금 새어 들어갔습니다. 다음 장에서는 렉시컬 스코프에 대해 더 깊이 탐구하고 그 구멍을 메울 것입니다.

도전 과제

  1. 우리 인터프리터는 함수에 전달되는 인자의 수가 함수가 기대하는 매개변수의 수와 일치하는지 신중하게 확인합니다. 이 검사는 모든 호출에서 런타임에 수행되므로 성능 비용이 발생합니다. Smalltalk 구현체는 그런 문제가 없습니다. 왜 그럴까요?

  2. Lox의 함수 선언 문법은 두 가지 독립적인 연산을 수행합니다. 함수를 생성하고 그 함수를 이름에 바인딩합니다. 이는 함수에 이름을 연결하려는 일반적인 경우에 유용성을 향상시킵니다. 하지만 함수형 스타일 코드에서는 함수를 생성하여 다른 함수에 즉시 전달하거나 반환하고 싶을 때가 많습니다. 이 경우 이름이 필요하지 않습니다.

    함수형 스타일을 장려하는 언어들은 일반적으로 함수를 이름에 바인딩하지 않고 생성하는 표현식 문법인 **익명 함수(anonymous functions)** 또는 **람다(lambdas)**를 지원합니다. Lox에 익명 함수 문법을 추가하여 다음 코드가 작동하도록 하세요:

    fun thrice(fn) {
      for (var i = 1; i <= 3; i = i + 1) {
        fn(i);
      }
    }
    
    thrice(fun (a) {
      print a;
    });
    // "1".
    // "2".
    // "3".
    

    표현식 문(expression statement)에 익명 함수 표현식이 나타나는 까다로운 경우는 어떻게 처리하시겠습니까?

    fun () {};
    
  3. 이 프로그램은 유효합니까?

    fun scope(a) {
      var a = "local";
    }
    

    다시 말해, 함수의 매개변수가 지역 변수와 *동일한* 스코프에 있습니까, 아니면 외부 스코프에 있습니까? Lox는 어떻게 처리합니까? 익숙한 다른 언어들은 어떻습니까? 언어가 *어떻게* 처리해야 한다고 생각하십니까?