{ }codelper
作用域字典查找递归P0245

用静态作用域和动态作用域各运行一次

把用一种小语言写的程序按两种变量作用域规则分别运行,并把结果并排输出。
程序有 n 行,每行一条命令。
从 def f 行到 end 行是函数 f 的定义,中间的行是 f 的函数体。
函数定义只出现在最外层,不在函数里定义函数。
函数定义之外的行是最外层命令,从上到下依次执行。
函数定义本身不执行,函数的定义无论在程序的哪里都可以调用。
变量放在“格”里。
最外层命令使用全局格。
每次调用函数都新建一个空格,在这个格里执行该函数的函数体。
函数结束后这个格就消失。
命令有五种。
- var x v:在当前格里创建变量 x 并放入值 v。
当前格里已经有 x 时,只把它的值改为 v。
- set x v:查找 x,把它的值改为 v。
- add x v:查找 x,给它的值加上 v。
- print x:查找 x 并输出它的值。
找不到时输出 ?。
- call f:调用函数 f。
f 结束后继续下一条命令。
set 和 add 找不到 x 时什么也不做,不创建新变量。
“查找”是按规定的顺序查看各个格,选出第一个含有 x 的格。
这个顺序就是作用域规则。
静态作用域下,先看当前格,再看全局格。
动态作用域下,依次看当前格、调用当前函数的函数的格、调用那个函数的函数的格……最后看全局格。
在最外层,当前格就是全局格。
变量名和函数名分开算,同一个名字可以同时用作变量和函数。
把程序按静态作用域运行一次,按动态作用域运行一次,各自从初始状态开始。
两次运行中 print 执行的次数相同。
每执行一次 print,就在一行中输出静态作用域下的输出和动态作用域下的输出,用一个空格隔开。

输入
第一行给出 n,接下来 n 行每行一条命令。
1 ≤ n ≤ 1,000。
名字是以小写英文字母开头的小写英文字母和数字,长 1~10。
所有 v 以及运行中变量取到的所有值都是 -10^9 到 10^9 之间的整数。
每个 def 都有配对的 end,同名函数不会定义两次,call 调用的函数都有定义。
函数不会直接或经由其他函数调用自己。
两次运行中,执行的命令(含 call)都不超过 50,000 条,同时未结束的函数调用不超过 30 个,执行的 print 有 1 到 5,000 个
输出
与 print 执行次数相同的行数
样例 1 · stdin
13
var x 1
def show
print x
end
def change
var x 2
call show
set x 3
call show
end
call change
print x
call show
stdout
1 2
1 3
1 1
1 1
样例 2 · stdin
21
def inc
add n 1
print n
end
def run
var n 10
call inc
call inc
end
var n 0
call run
print n
def peek
print m
end
def outer
var m 5
call peek
end
call outer
call peek
stdout
1 11
2 12
2 0
? 5
? ?
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

8 种语言都是静态作用域。
函数里能看到的名字取决于“写在哪里”,而不是“从哪里调用”。
动态作用域可以在老式 Lisp、Emacs Lisp(默认)、bash 函数的 local、Perl 的 local 中看到。
第一个例子中的 show 在静态作用域下总是看到全局的 x(1),在动态作用域下看到调用它的 change 的 x(2、3)。
解法是每个格放一个字典,把格堆在列表(栈)里:Python dict 列表,JS・TS Map[],Java List<Map<String, Integer>>,C# List<Dictionary<string, int>>,C++ std::vector<std::unordered_map<…>>,Craykon 字典() 列表。
C 没有字典,先把名字换成编号,把(编号, 值)对排成一列堆起来,并记住每个格的起始位置。
call 压入一个格,递归执行函数体后再弹出这个格。
静态查找只看最上面的格和 0 号格,动态查找从最上面依次看到 0 号格。
和各语言的实际规则比较:Python 把函数里被赋值的名字当作整个函数的局部变量,要修改外层的需要 global・nonlocal。
本题的 set 则相反,修改找到的那个格。
Craykon 中,在函数里给最上层(函数外)创建的变量赋值会改变那个变量,函数里第一次创建的名字属于该函数,在外面使用会被检查器拦下。
JS 的 var 是函数作用域,let・const 是块作用域。
C・C++・Java・C# 是块作用域,Java・C# 禁止在内层块中重新声明与外层局部变量同名的变量。
陷阱 ① 动态作用域只查当前格和全局格,或者静态作用域却查到调用者的格。
② 动态作用域从全局格开始查(最近的格优先)。
③ 函数结束后仍保留格,下次调用看到旧值(类似 C 的 static 局部变量)。
④ set 找不到时在当前格新建变量(Python 式赋值)。
⑤ 让 var 修改已经可见的变量(var 总是用当前格)。

Pythonmain.py · 53 行
import sys lines = sys.stdin.read().split("\n")n = int(lines[0])funcs = {}   # 함수 이름 → 몸체 명령들main = []    # 맨 바깥 명령들body = Nonefor line in lines[1:1 + n]:    t = line.split()    if t[0] == "def":        body = funcs[t[1]] = []    elif t[0] == "end":        body = None    else:        (main if body is None else body).append(t)  def run(dynamic):    frames = [{}]  # frames[0]은 맨 바깥(전역), 끝이 지금 실행 중인 함수의 칸    printed = []     def find(name):        """이름을 가진 칸(사전)을 찾는다. 없으면 None."""        # 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역        order = reversed(frames) if dynamic else (frames[-1], frames[0])        for frame in order:            if name in frame:                return frame        return None     def execute(cmds):        for cmd in cmds:            op = cmd[0]            if op == "var":                frames[-1][cmd[1]] = int(cmd[2])            elif op in ("set", "add"):                frame = find(cmd[1])                if frame is not None:                    frame[cmd[1]] = int(cmd[2]) if op == "set" else frame[cmd[1]] + int(cmd[2])            elif op == "print":                frame = find(cmd[1])                printed.append("?" if frame is None else str(frame[cmd[1]]))            else:  # call                frames.append({})  # 부를 때마다 빈 칸에서 시작한다                execute(funcs[cmd[1]])                frames.pop()       # 끝나면 그 칸은 사라진다     execute(main)    return printed  for a, b in zip(run(False), run(True)):    print(a, b)
JavaScriptmain.js · 43 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const n = Number(lines[0]);const funcs = new Map(); // 함수 이름 → 몸체 명령들const main = []; // 맨 바깥 명령들let body = null;for (const line of lines.slice(1, 1 + n)) {  const t = line.trim().split(" ");  if (t[0] === "def") funcs.set(t[1], (body = []));  else if (t[0] === "end") body = null;  else (body ?? main).push(t);} function run(dynamic) {  const frames = [new Map()]; // frames[0]은 전역, 끝이 지금 실행 중인 함수의 칸  const printed = [];  // 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역  const find = (name) => {    const order = dynamic ? [...frames].reverse() : [frames[frames.length - 1], frames[0]];    return order.find((f) => f.has(name));  };  const execute = (cmds) => {    for (const [op, name, arg] of cmds) {      if (op === "var") {        frames[frames.length - 1].set(name, Number(arg));      } else if (op === "set" || op === "add") {        const f = find(name);        if (f) f.set(name, op === "set" ? Number(arg) : f.get(name) + Number(arg));      } else if (op === "print") {        const f = find(name);        printed.push(f ? String(f.get(name)) : "?");      } else {        frames.push(new Map()); // 부를 때마다 빈 칸에서 시작한다        execute(funcs.get(name));        frames.pop(); // 끝나면 그 칸은 사라진다      }    }  };  execute(main);  return printed;} const lex = run(false), dyn = run(true);console.log(lex.map((v, i) => `${v} ${dyn[i]}`).join("\n"));
TypeScriptmain.ts · 48 行
import { readFileSync } from "node:fs"; type Frame = Map<string, number>;const lines: string[] = readFileSync(0, "utf8").split("\n");const n = Number(lines[0]);const funcs = new Map<string, string[][]>(); // 함수 이름 → 몸체 명령들const main: string[][] = []; // 맨 바깥 명령들let body: string[][] | null = null;for (const line of lines.slice(1, 1 + n)) {  const t = line.trim().split(" ");  if (t[0] === "def") {    body = [];    funcs.set(t[1], body);  } else if (t[0] === "end") body = null;  else (body ?? main).push(t);} function run(dynamic: boolean): string[] {  const frames: Frame[] = [new Map()]; // frames[0]은 전역, 끝이 지금 실행 중인 함수의 칸  const printed: string[] = [];  // 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역  const find = (name: string): Frame | undefined => {    const order = dynamic ? [...frames].reverse() : [frames[frames.length - 1], frames[0]];    return order.find((f) => f.has(name));  };  const execute = (cmds: string[][]): void => {    for (const [op, name, arg] of cmds) {      if (op === "var") {        frames[frames.length - 1].set(name, Number(arg));      } else if (op === "set" || op === "add") {        const f = find(name);        if (f) f.set(name, op === "set" ? Number(arg) : (f.get(name) ?? 0) + Number(arg));      } else if (op === "print") {        const f = find(name);        printed.push(f ? String(f.get(name)) : "?");      } else {        frames.push(new Map()); // 부를 때마다 빈 칸에서 시작한다        execute(funcs.get(name) ?? []);        frames.pop(); // 끝나면 그 칸은 사라진다      }    }  };  execute(main);  return printed;} const lex = run(false), dyn = run(true);console.log(lex.map((v, i) => `${v} ${dyn[i]}`).join("\n"));
JavaMain.java · 78 行
import java.io.BufferedReader;import java.io.InputStreamReader;import java.io.IOException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; public class Main {    static Map<String, List<String[]>> funcs = new HashMap<>(); // 함수 이름 → 몸체 명령들    static List<Map<String, Integer>> frames;                   // 0번이 전역, 끝이 지금 함수의 칸    static boolean dynamic;    static List<String> printed;     // 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역    static Map<String, Integer> find(String name) {        int top = frames.size() - 1;        if (dynamic) {            for (int i = top; i >= 0; i--)                if (frames.get(i).containsKey(name)) return frames.get(i);            return null;        }        if (frames.get(top).containsKey(name)) return frames.get(top);        if (frames.get(0).containsKey(name)) return frames.get(0);        return null;    }     static void execute(List<String[]> cmds) {        for (String[] c : cmds) {            switch (c[0]) {                case "var" -> frames.get(frames.size() - 1).put(c[1], Integer.parseInt(c[2]));                case "set", "add" -> {                    Map<String, Integer> f = find(c[1]);                    if (f != null) f.put(c[1], c[0].equals("set") ? Integer.parseInt(c[2]) : f.get(c[1]) + Integer.parseInt(c[2]));                }                case "print" -> {                    Map<String, Integer> f = find(c[1]);                    printed.add(f == null ? "?" : String.valueOf(f.get(c[1])));                }                default -> {                    frames.add(new HashMap<>()); // 부를 때마다 빈 칸에서 시작한다                    execute(funcs.get(c[1]));                    frames.remove(frames.size() - 1); // 끝나면 그 칸은 사라진다                }            }        }    }     static List<String> run(List<String[]> main, boolean dyn) {        dynamic = dyn;        frames = new ArrayList<>();        frames.add(new HashMap<>());        printed = new ArrayList<>();        execute(main);        return printed;    }     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(in.readLine().trim());        List<String[]> main = new ArrayList<>(), body = null;        for (int i = 0; i < n; i++) {            String[] t = in.readLine().trim().split(" ");            if (t[0].equals("def")) {                body = new ArrayList<>();                funcs.put(t[1], body);            } else if (t[0].equals("end")) {                body = null;            } else {                (body == null ? main : body).add(t);            }        }        List<String> lex = run(main, false), dyn = run(main, true);        StringBuilder out = new StringBuilder();        for (int i = 0; i < lex.size(); i++) out.append(lex.get(i)).append(' ').append(dyn.get(i)).append('\n');        System.out.print(out);    }}
C#Main.cs · 81 行
using System;using System.Collections.Generic;using System.Text; class Program{    static Dictionary<string, List<string[]>> funcs = new(); // 함수 이름 → 몸체 명령들    static List<Dictionary<string, int>> frames = new();     // 0번이 전역, 끝이 지금 함수의 칸    static bool dynamic;    static List<string> printed = new();     // 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역    static Dictionary<string, int> Find(string name)    {        int top = frames.Count - 1;        if (dynamic)        {            for (int i = top; i >= 0; i--)                if (frames[i].ContainsKey(name)) return frames[i];            return null;        }        if (frames[top].ContainsKey(name)) return frames[top];        if (frames[0].ContainsKey(name)) return frames[0];        return null;    }     static void Execute(List<string[]> cmds)    {        foreach (string[] c in cmds)        {            switch (c[0])            {                case "var":                    frames[^1][c[1]] = int.Parse(c[2]);                    break;                case "set":                case "add":                    var f = Find(c[1]);                    if (f != null) f[c[1]] = c[0] == "set" ? int.Parse(c[2]) : f[c[1]] + int.Parse(c[2]);                    break;                case "print":                    var g = Find(c[1]);                    printed.Add(g == null ? "?" : g[c[1]].ToString());                    break;                default:                    frames.Add(new Dictionary<string, int>()); // 부를 때마다 빈 칸에서 시작한다                    Execute(funcs[c[1]]);                    frames.RemoveAt(frames.Count - 1); // 끝나면 그 칸은 사라진다                    break;            }        }    }     static List<string> Run(List<string[]> main, bool dyn)    {        dynamic = dyn;        frames = new List<Dictionary<string, int>> { new() };        printed = new List<string>();        Execute(main);        return printed;    }     static void Main()    {        int n = int.Parse(Console.ReadLine().Trim());        var main = new List<string[]>();        List<string[]> body = null;        for (int i = 0; i < n; i++)        {            string[] t = Console.ReadLine().Trim().Split(' ');            if (t[0] == "def") funcs[t[1]] = body = new List<string[]>();            else if (t[0] == "end") body = null;            else (body ?? main).Add(t);        }        var lex = Run(main, false);        var dyn = Run(main, true);        var sb = new StringBuilder();        for (int i = 0; i < lex.Count; i++) sb.Append(lex[i]).Append(' ').Append(dyn[i]).Append('\n');        Console.Write(sb);    }}
C++main.cpp · 76 行
#include <iostream>#include <sstream>#include <string>#include <unordered_map>#include <vector> using Frame = std::unordered_map<std::string, int>;struct Cmd {    std::string op, name;    int arg;}; std::unordered_map<std::string, std::vector<Cmd>> funcs; // 함수 이름 → 몸체 명령들std::vector<Frame> frames;                               // 0번이 전역, 끝이 지금 함수의 칸bool dynamic;std::vector<std::string> printed; // 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역Frame *find(const std::string &name) {    if (dynamic) {        for (auto it = frames.rbegin(); it != frames.rend(); ++it)            if (it->count(name)) return &*it;        return nullptr;    }    if (frames.back().count(name)) return &frames.back();    if (frames.front().count(name)) return &frames.front();    return nullptr;} void execute(const std::vector<Cmd> &cmds) {    for (const Cmd &c : cmds) {        if (c.op == "var") {            frames.back()[c.name] = c.arg;        } else if (c.op == "set" || c.op == "add") {            if (Frame *f = find(c.name)) (*f)[c.name] = c.op == "set" ? c.arg : (*f)[c.name] + c.arg;        } else if (c.op == "print") {            Frame *f = find(c.name);            printed.push_back(f ? std::to_string(f->at(c.name)) : "?");        } else {            // 부를 때마다 빈 칸에서 시작한다. push_back이 vector를 옮길 수 있어 칸을 가리키는 포인터는 오래 들지 않는다            frames.emplace_back();            execute(funcs[c.name]);            frames.pop_back(); // 끝나면 그 칸은 사라진다        }    }} std::vector<std::string> run(const std::vector<Cmd> &main, bool dyn) {    dynamic = dyn;    frames.assign(1, Frame{});    printed.clear();    execute(main);    return printed;} int main() {    int n;    std::string line;    std::getline(std::cin, line);    n = std::stoi(line);    std::vector<Cmd> main;    std::vector<Cmd> *body = nullptr;    for (int i = 0; i < n; i++) {        std::getline(std::cin, line);        std::istringstream ss(line);        Cmd c{"", "", 0};        ss >> c.op >> c.name >> c.arg;        if (c.op == "def") body = &funcs[c.name];        else if (c.op == "end") body = nullptr;        else (body ? *body : main).push_back(c);    }    auto lex = run(main, false), dyn = run(main, true);    std::string out;    for (size_t i = 0; i < lex.size(); i++) out += lex[i] + " " + dyn[i] + "\n";    std::cout << out;}
Cmain.c · 121 行
#include <stdio.h>#include <string.h> enum { VAR, SET, ADD, PRINT, CALL };typedef struct { int op, id, arg; } Cmd; /* id: 변수 번호, call이면 함수 번호 */ static char vname[1000][11], fname[1000][11];static int nv, nf;static Cmd cmds[1000];static int ncmd;static int fstart[1000], fend[1000]; /* 함수 몸체는 cmds[fstart, fend) */static int mainlist[1000], nmain;    /* 맨 바깥 명령의 번호들 */ /* 칸 하나는 (변수 번호, 값) 묶음들. 모든 칸을 한 줄로 쌓고, 칸마다 시작 자리를 둔다 */static int ent_id[1000], ent_val[1000], nent;static int frame_at[64], depth; /* frame_at[d]: d번 칸의 시작. 0번이 전역 */static int dynamic;static char printed[2][50000][12];static int nprinted; static int intern(char names[][11], int *count, const char *s) {    for (int i = 0; i < *count; i++)        if (strcmp(names[i], s) == 0) return i;    strcpy(names[*count], s);    return (*count)++;} /* d번 칸에서 변수 id의 자리(없으면 -1) */static int in_frame(int d, int id) {    int end = d == depth ? nent : frame_at[d + 1];    for (int e = frame_at[d]; e < end; e++)        if (ent_id[e] == id) return e;    return -1;} /* 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역 */static int find(int id) {    int e;    if (dynamic) {        for (int d = depth; d >= 0; d--)            if ((e = in_frame(d, id)) >= 0) return e;        return -1;    }    if ((e = in_frame(depth, id)) >= 0) return e;    return depth > 0 ? in_frame(0, id) : -1;} static void exec_one(const Cmd *c); static void call(int f) {    depth++;    frame_at[depth] = nent; /* 부를 때마다 빈 칸에서 시작한다 */    for (int i = fstart[f]; i < fend[f]; i++) exec_one(&cmds[i]);    nent = frame_at[depth]; /* 끝나면 그 칸은 사라진다 */    depth--;} static void exec_one(const Cmd *c) {    int e;    switch (c->op) {    case VAR:        if ((e = in_frame(depth, c->id)) < 0) {            e = nent++;            ent_id[e] = c->id;        }        ent_val[e] = c->arg;        break;    case SET:    case ADD:        if ((e = find(c->id)) >= 0) ent_val[e] = c->op == SET ? c->arg : ent_val[e] + c->arg;        break;    case PRINT:        e = find(c->id);        if (e < 0) strcpy(printed[dynamic][nprinted++], "?");        else snprintf(printed[dynamic][nprinted++], 12, "%d", ent_val[e]);        break;    case CALL:        call(c->id);        break;    }} int main(void) {    int n, cur = -1;    scanf("%d", &n);    for (int i = 0; i < n; i++) {        char op[8], name[11];        scanf("%7s", op);        if (strcmp(op, "end") == 0) {            fend[cur] = ncmd;            cur = -1;            continue;        }        scanf("%10s", name);        if (strcmp(op, "def") == 0) {            cur = intern(fname, &nf, name);            fstart[cur] = ncmd;            continue;        }        Cmd c = {0, 0, 0};        if (strcmp(op, "call") == 0) {            c.op = CALL;            c.id = intern(fname, &nf, name); /* 뒤에서 정의되는 함수도 번호를 먼저 받는다 */        } else {            c.op = strcmp(op, "var") == 0 ? VAR : strcmp(op, "set") == 0 ? SET : strcmp(op, "add") == 0 ? ADD : PRINT;            c.id = intern(vname, &nv, name);            if (c.op != PRINT) scanf("%d", &c.arg);        }        if (cur < 0) mainlist[nmain++] = ncmd;        cmds[ncmd++] = c;    }    int count = 0;    for (dynamic = 0; dynamic <= 1; dynamic++) {        nent = depth = nprinted = 0;        frame_at[0] = 0;        for (int i = 0; i < nmain; i++) exec_one(&cmds[mainlist[i]]);        count = nprinted;    }    for (int i = 0; i < count; i++) printf("%s %s\n", printed[0][i], printed[1][i]);    return 0;}
Craykonmain.cray · 116 行
n = 控制台.读行().转数字();funcs = 字典(); // 함수 이름 → 몸체 명령들main = []; // 맨 바깥 명령들body = 空;循环 (i = 0; i < n; i++){    t = 控制台.读行().去空白().分割(" ");    如果 (t[0] == "def")    {        body = [];        funcs[t[1]] = body;    }    否则 如果 (t[0] == "end")    {        body = 空;    }    否则 如果 (body == 空)    {        main.添加(t);    }    否则    {        body.添加(t);    }} frames = []; // 0번이 전역, 끝이 지금 실행 중인 함수의 칸dynamic = 假;printed = []; // 이름을 가진 칸의 번호(없으면 -1)// 정적: 지금 칸 → 전역. 동적: 지금 칸 → 부른 함수의 칸 → … → 전역찾기(name){    top = frames.数量 - 1;    如果 (dynamic)    {        循环 (d = top; d >= 0; d--)        {            如果 (frames[d].包含(name))            {                返回 d;            }        }        返回 -1;    }    如果 (frames[top].包含(name))    {        返回 top;    }    如果 (frames[0].包含(name))    {        返回 0;    }    返回 -1;} 실행(cmds){    遍历 (c 在 cmds)    {        选择 (c[0])        {            情况 "var":                frames[frames.数量 - 1][c[1]] = c[2].转数字();                跳出;            情况 "set":            情况 "add":                d = 찾기(c[1]);                如果 (d >= 0)                {                    如果 (c[0] == "set")                    {                        frames[d][c[1]] = c[2].转数字();                    }                    否则                    {                        frames[d][c[1]] = frames[d][c[1]] + c[2].转数字();                    }                }                跳出;            情况 "print":                d = 찾기(c[1]);                如果 (d >= 0)                {                    printed.添加("" + frames[d][c[1]]);                }                否则                {                    printed.添加("?");                }                跳出;            默认:                frames.添加(字典()); // 부를 때마다 빈 칸에서 시작한다                실행(funcs[c[1]]);                frames.按位置移除(frames.数量 - 1); // 끝나면 그 칸은 사라진다                跳出;        }    }} 돌리기(동적인가){    dynamic = 동적인가;    frames = [字典()];    printed = [];    실행(main);    返回 printed;} lex = 돌리기(假);dyn = 돌리기(真);循环 (i = 0; i < lex.数量; i++){    控制台.输出(lex[i] + " " + dyn[i]);}