diff --git a/src/scope.ts b/src/scope.ts index f8a4579..2dd9f28 100644 --- a/src/scope.ts +++ b/src/scope.ts @@ -30,6 +30,15 @@ export class Scope { has(name: string): boolean { return this.locals.has(name) || this.parent?.has(name) || false } + + vars(): string[] { + const vars = new Set(this.parent?.vars()) + + for (const name of this.locals.keys()) + vars.add(name) + + return [...vars].sort() + } } diff --git a/src/vm.ts b/src/vm.ts index 24ea531..aae6be6 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -48,6 +48,14 @@ export class VM { } } + has(name: string): boolean { + return this.scope.has(name) + } + + vars(): string[] { + return this.scope.vars() + } + get(name: string) { return this.scope.get(name) } diff --git a/tests/scope.test.ts b/tests/scope.test.ts new file mode 100644 index 0000000..417e6e0 --- /dev/null +++ b/tests/scope.test.ts @@ -0,0 +1,243 @@ +import { test, expect } from "bun:test" +import { Scope, toValue } from "#reef" + +test("Scope - create empty scope", () => { + const scope = new Scope() + expect(scope.parent).toBeUndefined() + expect(scope.locals.size).toBe(0) +}) + +test("Scope - create child scope with parent", () => { + const parent = new Scope() + const child = new Scope(parent) + + expect(child.parent).toBe(parent) + expect(child.locals.size).toBe(0) +}) + +test("Scope - set and get variable in same scope", () => { + const scope = new Scope() + const value = toValue(42) + + scope.set("x", value) + expect(scope.get("x")).toBe(value) +}) + +test("Scope - get returns undefined for non-existent variable", () => { + const scope = new Scope() + expect(scope.get("x")).toBeUndefined() +}) + +test("Scope - set updates existing variable in same scope", () => { + const scope = new Scope() + + scope.set("x", toValue(10)) + expect(scope.get("x")).toEqual({ type: "number", value: 10 }) + + scope.set("x", toValue(20)) + expect(scope.get("x")).toEqual({ type: "number", value: 20 }) +}) + +test("Scope - get searches parent scope", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(42)) + expect(child.get("x")).toEqual({ type: "number", value: 42 }) +}) + +test("Scope - get searches multiple levels up", () => { + const grandparent = new Scope() + const parent = new Scope(grandparent) + const child = new Scope(parent) + + grandparent.set("x", toValue(100)) + expect(child.get("x")).toEqual({ type: "number", value: 100 }) +}) + +test("Scope - child updates parent variable when it exists (no shadowing)", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(10)) + child.set("x", toValue(20)) + + // set() updates parent's variable, not shadow it + expect(parent.get("x")).toEqual({ type: "number", value: 20 }) + expect(child.get("x")).toEqual({ type: "number", value: 20 }) + expect(child.locals.has("x")).toBe(false) +}) + +test("Scope - set updates parent scope variable when it exists", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(10)) + child.set("x", toValue(20)) + + // Should update parent's variable, not create new local one + expect(parent.get("x")).toEqual({ type: "number", value: 20 }) + expect(child.get("x")).toEqual({ type: "number", value: 20 }) + expect(child.locals.has("x")).toBe(false) +}) + +test("Scope - set creates new local variable when not found in parent", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(10)) + child.set("y", toValue(20)) + + expect(parent.get("x")).toEqual({ type: "number", value: 10 }) + expect(parent.get("y")).toBeUndefined() + expect(child.get("x")).toEqual({ type: "number", value: 10 }) + expect(child.get("y")).toEqual({ type: "number", value: 20 }) + expect(child.locals.has("y")).toBe(true) +}) + +test("Scope - has returns true for variable in current scope", () => { + const scope = new Scope() + scope.set("x", toValue(42)) + + expect(scope.has("x")).toBe(true) + expect(scope.has("y")).toBe(false) +}) + +test("Scope - has returns true for variable in parent scope", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(42)) + + expect(child.has("x")).toBe(true) + expect(child.has("y")).toBe(false) +}) + +test("Scope - has searches multiple levels up", () => { + const grandparent = new Scope() + const parent = new Scope(grandparent) + const child = new Scope(parent) + + grandparent.set("x", toValue(100)) + + expect(child.has("x")).toBe(true) + expect(parent.has("x")).toBe(true) + expect(grandparent.has("x")).toBe(true) +}) + +test("Scope.vars() - returns empty array for empty scope", () => { + const scope = new Scope() + expect(scope.vars()).toEqual([]) +}) + +test("Scope.vars() - returns single variable from current scope", () => { + const scope = new Scope() + scope.set("x", toValue(42)) + + expect(scope.vars()).toEqual(["x"]) +}) + +test("Scope.vars() - returns multiple variables from current scope", () => { + const scope = new Scope() + scope.set("x", toValue(1)) + scope.set("y", toValue(2)) + scope.set("z", toValue(3)) + + const vars = scope.vars() + expect(vars.length).toBe(3) + expect(vars).toContain("x") + expect(vars).toContain("y") + expect(vars).toContain("z") +}) + +test("Scope.vars() - includes variables from parent scope", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(1)) + child.set("y", toValue(2)) + + const vars = child.vars() + expect(vars.length).toBe(2) + expect(vars).toContain("x") + expect(vars).toContain("y") +}) + +test("Scope.vars() - includes variables from multiple parent scopes", () => { + const grandparent = new Scope() + const parent = new Scope(grandparent) + const child = new Scope(parent) + + grandparent.set("x", toValue(1)) + parent.set("y", toValue(2)) + child.set("z", toValue(3)) + + const vars = child.vars() + expect(vars.length).toBe(3) + expect(vars).toContain("x") + expect(vars).toContain("y") + expect(vars).toContain("z") +}) + +test("Scope.vars() - no duplicates when child updates parent variable", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(10)) + child.set("x", toValue(20)) // Updates parent, doesn't create local + + // Only one "x" since child doesn't have its own local x + const vars = child.vars() + expect(vars).toEqual(["x"]) +}) + +test("Scope.vars() - can't have duplicates if manually set in locals", () => { + const parent = new Scope() + const child = new Scope(parent) + + parent.set("x", toValue(10)) + child.locals.set("x", toValue(20)) + const vars = child.vars() + expect(vars).toEqual(["x"]) + + expect(child.get("x")).toEqual({ type: "number", value: 20 }) + expect(parent.get("x")).toEqual({ type: "number", value: 10 }) +}) + +test("Scope.vars() - handles deep scope chains", () => { + let scope = new Scope() + + // Create a deep chain: level0 -> level1 -> ... -> level5 + for (let i = 0; i < 5; i++) { + scope.set(`var${i}`, toValue(i)) + scope = new Scope(scope) + } + + // Final scope should have all variables from the chain + const vars = scope.vars() + expect(vars.length).toBe(5) + expect(vars).toContain("var0") + expect(vars).toContain("var1") + expect(vars).toContain("var2") + expect(vars).toContain("var3") + expect(vars).toContain("var4") +}) + +test("Scope - can store and retrieve functions", () => { + const scope = new Scope() + + const fnValue = { + type: 'function' as const, + value: '' as const, + params: ['x'], + defaults: {}, + body: 0, + variadic: false, + named: false, + parentScope: scope + } + + scope.set("myFunc", fnValue) + expect(scope.get("myFunc")).toBe(fnValue) + expect(scope.get("myFunc")?.type).toBe("function") +})