From c027c87dc48cc517fe9e85fe1012c36a4a87dca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Mon, 8 Jan 2024 22:23:50 +0100 Subject: [PATCH] Reuse a global state with standard library already loaded in tests, and associated branch handling fixes --- anselme/ast/Branched.lua | 13 +++-- anselme/ast/Environment.lua | 49 +++++++++++-------- anselme/ast/Function.lua | 7 +-- anselme/state/State.lua | 14 +++--- test/results/exported variable nested.ans | 2 +- test/results/for invalid iterator.ans | 2 +- test/results/function random.ans | 8 +-- test/results/function scope wrong.ans | 2 +- ...nction separate variable from variants.ans | 12 ++--- test/run.lua | 31 ++++++------ 10 files changed, 80 insertions(+), 60 deletions(-) diff --git a/anselme/ast/Branched.lua b/anselme/ast/Branched.lua index 563f7fd..9bb66b5 100644 --- a/anselme/ast/Branched.lua +++ b/anselme/ast/Branched.lua @@ -20,15 +20,22 @@ Branched = ast.abstract.Runtime { return not not self.value[state.branch_id] end, get = function(self, state) - return self.value[state.branch_id] or self.value[state.source_branch_id] + local branch = state + repeat + if self.value[branch.branch_id] then + return self.value[branch.branch_id] + end + branch = branch.source_branch + until not branch + error("no value assigned in this branch or any parent branch") end, set = function(self, state, value) self.value[state.branch_id] = value end, _merge = function(self, state, cache) local val = self.value[state.branch_id] - if val then - self.value[state.source_branch_id] = val + if val and state.source_branch then + self.value[state.source_branch.branch_id] = val self.value[state.branch_id] = nil val:merge(state, cache) end diff --git a/anselme/ast/Environment.lua b/anselme/ast/Environment.lua index 27c3f65..681a288 100644 --- a/anselme/ast/Environment.lua +++ b/anselme/ast/Environment.lua @@ -75,16 +75,16 @@ local Environment = ast.abstract.Runtime { partial = nil, -- { [name string] = true, ... } export = nil, -- bool - _lookup_cache = nil, -- { [name string] = variable metadata, ... } - _lookup_cache_current = nil, -- { [name string] = variable metadata, ... } + _lookup_cache = nil, -- Table of { [identifier] = variable metadata, ... } + _lookup_cache_current = nil, -- Table of { [identifier] = variable metadata, ... } init = function(self, state, parent, partial_names, is_export) self.variables = Table:new(state) self.parent = parent self.partial = partial_names self.export = is_export - self._lookup_cache = {} - self._lookup_cache_current = {} + self._lookup_cache = Table:new(state) + self._lookup_cache_current = Table:new(state) end, -- precache variable and return its variable metadata -- when cached, if a variable is defined in a parent scope after it has been cached here from a higher parent, it will not be used in this env @@ -101,6 +101,8 @@ local Environment = ast.abstract.Runtime { fn(self.parent, ...) end fn(self.variables, ...) + fn(self._lookup_cache, ...) + fn(self._lookup_cache_current, ...) end, _format = function(self, state) return "" @@ -117,9 +119,10 @@ local Environment = ast.abstract.Runtime { return self.parent:define(state, symbol, exp) end local variable = VariableMetadata:new(state, symbol, exp) - self.variables:set(state, symbol:to_identifier(), variable) - self._lookup_cache[name] = variable - self._lookup_cache_current[name] = variable + local identifier = symbol:to_identifier() + self.variables:set(state, identifier, variable) + self._lookup_cache:set(state, identifier, variable) + self._lookup_cache_current:set(state, identifier, variable) end, -- define or redefine new overloadable variable in current environment, inheriting existing overload variants from (parent) scopes define_overloadable = function(self, state, symbol, exp) @@ -151,40 +154,46 @@ local Environment = ast.abstract.Runtime { end, -- lookup variable in current or parent scope, cache the result + -- returns nil if undefined _lookup = function(self, state, identifier) - local name = identifier.name local _cache = self._lookup_cache - if _cache[name] == nil then + local var + if not _cache:has(state, identifier) then if self.variables:has(state, identifier) then - _cache[name] = self.variables:get(state, identifier) + var = self.variables:get(state, identifier) elseif self.parent then - _cache[name] = self.parent:_lookup(state, identifier) + var = self.parent:_lookup(state, identifier) end + if var then _cache:set(state, identifier, var) end + else + var = _cache:get(state, identifier) end - local var = _cache[name] if var and not var:undefined(state) then return var end - return nil end, + -- lookup variable in current scope, cache the result + -- returns nil if undefined _lookup_in_current = function(self, state, symbol) - local name = symbol.string + local identifier = symbol:to_identifier() local _cache = self._lookup_cache_current - if _cache[name] == nil then - local identifier = symbol:to_identifier() + local var + if not _cache:has(state, identifier) then + local name = symbol.string if self.variables:has(state, identifier) then - _cache[name] = self.variables:get(state, identifier) + var = self.variables:get(state, identifier) elseif self.parent then if (self.partial and not self.partial[name]) or (self.export ~= symbol.exported) then - _cache[name] = self.parent:_lookup_in_current(state, symbol) + var = self.parent:_lookup_in_current(state, symbol) end end + if var then _cache:set(state, identifier, var) end + else + var = _cache:get(state, identifier) end - local var = _cache[name] if var and not var:undefined(state) then return var end - return nil end, -- returns bool if variable defined in current or parent environment diff --git a/anselme/ast/Function.lua b/anselme/ast/Function.lua index e186c49..9ef9a1c 100644 --- a/anselme/ast/Function.lua +++ b/anselme/ast/Function.lua @@ -1,7 +1,7 @@ -- note: functions only appear in non-evaluated nodes! once evaluated, they always become closures local ast = require("anselme.ast") -local ReturnBoundary, Environment, Identifier +local ReturnBoundary, Identifier local operator_priority = require("anselme.common").operator_priority @@ -62,7 +62,8 @@ Function = ast.abstract.Overloadable { end local upvalues = {} for _, identifier in ipairs(used_identifiers) do - table.insert(upvalues, scope:precache(state, identifier)) + local var = scope:precache(state, identifier) + if var then table.insert(upvalues, var) end end return Function:new(self.parameters:eval(state), self.expression, scope, upvalues) @@ -159,7 +160,7 @@ Function = ast.abstract.Overloadable { } package.loaded[...] = Function -ReturnBoundary, Environment, Identifier = ast.ReturnBoundary, ast.Environment, ast.Identifier +ReturnBoundary, Identifier = ast.ReturnBoundary, ast.Identifier resume_manager = require("anselme.state.resume_manager") calling_environment_manager = require("anselme.state.calling_environment_manager") diff --git a/anselme/state/State.lua b/anselme/state/State.lua index e95edd7..938066a 100644 --- a/anselme/state/State.lua +++ b/anselme/state/State.lua @@ -19,12 +19,12 @@ local State State = class { type = "anselme state", - init = function(self, branch_from) + init = function(self, branch_from, branch_id) -- create a new branch from an existing state -- note: the existing state must not currently have an active script if branch_from then - self.branch_id = uuid() - self.source_branch_id = branch_from.branch_id + self.branch_id = branch_id or uuid() + self.source_branch = branch_from self.scope = ScopeStack:new(self, branch_from) event_manager:reset(self) -- events are isolated per branch @@ -74,16 +74,16 @@ State = class { --- Name of the branch associated to this State. branch_id = "main", - --- Name of the branch this State was branched from. - source_branch_id = "main", + --- State this State was branched from. + source_branch = nil, --- Return a new branch of this State. -- -- Branches act as indepent copies of this State where any change will not be reflected in the source State until it is merged back into the source branch. -- Note: probably makes the most sense to create branches from the main State only. - branch = function(self) + branch = function(self, branch_id) assert(not self:active(), "can't branch while a script is active") - return State:new(self) + return State:new(self, branch_id) end, --- Merge everything that was changed in this branch back into the main State branch. -- diff --git a/test/results/exported variable nested.ans b/test/results/exported variable nested.ans index 1063c24..7f07e77 100644 --- a/test/results/exported variable nested.ans +++ b/test/results/exported variable nested.ans @@ -5,7 +5,7 @@ --- text --- | {}"" {}"42" {}"" | --- error --- -identifier "z" is undefined in branch 1c25ebb8-5027-4ccf-105b9-370d83e78fd7 +identifier "z" is undefined in branch test/tests/exported variable nested.ans - run ↳ from test/tests/exported variable nested.ans:12:3 in identifier: z ↳ from test/tests/exported variable nested.ans:12:1 in text interpolation: | {z} | ↳ from ? in block: :f = ($() _)… diff --git a/test/results/for invalid iterator.ans b/test/results/for invalid iterator.ans index fac8276..1a6ded7 100644 --- a/test/results/for invalid iterator.ans +++ b/test/results/for invalid iterator.ans @@ -7,7 +7,7 @@ type check failure for parameter tuple • $(range::is range) (from stdlib/for.ans:19:1): type check failure for parameter range - • $(s::is struct) (from stdlib/for.ans:3:14): + • $(s::is struct) (from stdlib/for.ans:2:1): type check failure for parameter s ↳ from stdlib/for.ans:3:18 in call: iter(var) ↳ from stdlib/for.ans:3:12 in definition: :iterator = iter(var) diff --git a/test/results/function random.ans b/test/results/function random.ans index 0e30d56..7d223c2 100644 --- a/test/results/function random.ans +++ b/test/results/function random.ans @@ -1,15 +1,15 @@ --# run #-- --- text --- -| {}"a" | ---- text --- | {}"c" | --- text --- +| {}"a" | +--- text --- | {}"b" | --- text --- | {}"c" | --- text --- -| {}"c" | +| {}"a" | --- return --- () --# saved #-- -{"a.checkpoint":false, "a.run":1, "b.checkpoint":false, "b.run":1, "c.checkpoint":false, "c.run":3} \ No newline at end of file +{"a.checkpoint":false, "a.run":2, "b.checkpoint":false, "b.run":1, "c.checkpoint":false, "c.run":2} \ No newline at end of file diff --git a/test/results/function scope wrong.ans b/test/results/function scope wrong.ans index 58ae1b4..cee8143 100644 --- a/test/results/function scope wrong.ans +++ b/test/results/function scope wrong.ans @@ -1,6 +1,6 @@ --# run #-- --- error --- -identifier "b" is undefined in branch 1c25ebb8-5027-4ccf-105b9-370d83e78fd7 +identifier "b" is undefined in branch test/tests/function scope wrong.ans - run ↳ from test/tests/function scope wrong.ans:4:7 in identifier: b ↳ from test/tests/function scope wrong.ans:4:1 in text interpolation: | a: {b} | ↳ from ? in block: :a = ($() _)… diff --git a/test/results/function separate variable from variants.ans b/test/results/function separate variable from variants.ans index d7a5d7b..565f02c 100644 --- a/test/results/function separate variable from variants.ans +++ b/test/results/function separate variable from variants.ans @@ -7,17 +7,17 @@ expected 3 arguments, received 2 • $(s::is script, k::is string) (from stdlib/script.ans:41:1): type check failure for parameter s - • $(c::is function, s::is symbol) = v (from test/tests/function separate variable from variants.ans:10:4): + • $(c::is function, s::is symbol) = v (from stdlib/for.ans:2:1): expected 3 arguments, received 2 - • $(c::is function, s::is string) = v (from test/tests/function separate variable from variants.ans:10:4): + • $(c::is function, s::is string) = v (from stdlib/for.ans:2:1): expected 3 arguments, received 2 - • $(c::is function, s::is string) (from test/tests/function separate variable from variants.ans:10:4): + • $(c::is function, s::is string) (from stdlib/for.ans:2:1): type check failure for parameter c - • $(c::is environment, s::is symbol) = v (from test/tests/function separate variable from variants.ans:10:4): + • $(c::is environment, s::is symbol) = v (from stdlib/for.ans:2:1): expected 3 arguments, received 2 - • $(c::is environment, s::is string) = v (from test/tests/function separate variable from variants.ans:10:4): + • $(c::is environment, s::is string) = v (from stdlib/for.ans:2:1): expected 3 arguments, received 2 - • $(c::is environment, s::is string) (from test/tests/function separate variable from variants.ans:10:4): + • $(c::is environment, s::is string) (from stdlib/for.ans:2:1): type check failure for parameter c ↳ from test/tests/function separate variable from variants.ans:10:4 in call: f . "a" ↳ from test/tests/function separate variable from variants.ans:10:1 in text interpolation: | {f . "a"} = 2 | diff --git a/test/run.lua b/test/run.lua index 61cab2c..21ddbc1 100644 --- a/test/run.lua +++ b/test/run.lua @@ -64,6 +64,15 @@ local function run_loop(run_state, write_output, interactive) end end +-- create execution state +local global_state = anselme:new() +global_state:load_stdlib() +global_state:define("interrupt", "(code::is string)", function(state, code) state:interrupt(code:to_lua(state), "interrupt") return ast.Nil:new() end, true) +global_state:define("interrupt", "()", function(state) state:interrupt() return ast.Nil:new() end, true) +global_state:define("wait", "(duration::is number)", function(duration) coroutine.yield("wait", duration) end) +global_state:define("serialize", "(value)", function(state, value) return ast.String:new(value:serialize(state)) end, true) +global_state:define("deserialize", "(str::is string)", function(state, str) return ast.abstract.Node:deserialize(state, str.string) end, true) + -- run a test file and return the result local function run(path, interactive) local out = { "--# run #--" } @@ -73,23 +82,17 @@ local function run(path, interactive) end math.randomseed() - local state = anselme:new() - state:load_stdlib() - - state:define("interrupt", "(code::is string)", function(state, code) state:interrupt(code:to_lua(state), "interrupt") return ast.Nil:new() end, true) - state:define("interrupt", "()", function(state) state:interrupt() return ast.Nil:new() end, true) - state:define("wait", "(duration::is number)", function(duration) coroutine.yield("wait", duration) end) - state:define("run in new branch", "(code)", function(code) - local parallel_state = state:branch() + local state = global_state:branch(path) + state:define("run in new branch", "(code)", function(state, code) + local parallel_state = state.source_branch:branch() write_output("--# parallel script #--") - parallel_state:run(code, "parallel") + parallel_state:run(code.string, "parallel") run_loop(parallel_state, write_output, interactive) write_output("--# main script #--") - end) - state:define("serialize", "(value)", function(state, value) return ast.String:new(value:serialize(state)) end, true) - state:define("deserialize", "(str::is string)", function(state, str) return ast.abstract.Node:deserialize(state, str.string) end, true) + return ast.Nil:new() + end, true) - local run_state = state:branch() + local run_state = state:branch(path.." - run") local f = assert(io.open(path, "r")) local s, block = pcall(anselme.parse, f:read("a"), path) @@ -106,7 +109,7 @@ local function run(path, interactive) run_loop(run_state, write_output, interactive) if state:defined("post run check") then - local post_run_state = state:branch() + local post_run_state = state:branch(path.." - post run check") post_run_state:run("post run check!") write_output("--# post run check #--")