abortive experiment: keep definitions independent
Functions can refer to each other, but global variable initializers shouldn't. But this doesn't work. That comment keeps growing to capture more corner cases. Step back. What am I trying to achieve? I'm not trying to create a better abstraction for programming with. I'm trying to use an existing abstraction (LÖVE) without needing additional tools. I'm not supporting end-user programming, only end-programmer programming. What happens in a regular LÖVE program if you use a global before it's defined? You get an error, and you're on the hook to fix it. But it's obvious what's going on because a file has an obvious sequence of definitions. But what if you have multiple files? It's easy to lose track of order and we mostly don't care. The important property existing dev environments care about: merely editing a definition doesn't _change_ the order of top-level definitions. Let's just provide this guarantee. We'll no longer load definitions in order of their version. Just load definitions in the order they were created. Editing a definition doesn't change this order. Deleting and recreating a definition puts it at the end.
This commit is contained in:
parent
6274322412
commit
02cdf25c1d
93
live.lua
93
live.lua
|
@ -36,6 +36,7 @@ function live.initialize(arg)
|
|||
Live.history = {} -- array of filename roots corresponding to each numeric prefix
|
||||
Live.manifest = {} -- mapping from roots to numeric prefixes as of version Live.head
|
||||
live.freeze_all_existing_definitions()
|
||||
Live.initial_env = table.copy(_G)
|
||||
live.load_files_so_far()
|
||||
Live.previous_read = 0
|
||||
|
||||
|
@ -257,10 +258,12 @@ function live.run(buf)
|
|||
-- other commands go here
|
||||
else
|
||||
local definition_name = cmd
|
||||
-- pre-check
|
||||
if Live.frozen_definitions[definition_name] then
|
||||
live.send_to_driver('ERROR definition '..definition_name..' is part of Freewheeling infrastructure and cannot be safely edited live.')
|
||||
return
|
||||
end
|
||||
-- perform the real evaluation and send back any real errors
|
||||
local next_filename = live.versioned_filename(Live.next_version, definition_name)
|
||||
love.filesystem.write(next_filename, buf)
|
||||
table.insert(Live.history, definition_name)
|
||||
|
@ -273,6 +276,67 @@ function live.run(buf)
|
|||
live.send_to_driver('ERROR '..tostring(err))
|
||||
return
|
||||
end
|
||||
-- Try running in bare environment to catch any order-dependence in
|
||||
-- definitions. The basic intent is to avoid this situation:
|
||||
-- define X = 3
|
||||
-- define Y = X+1
|
||||
-- edit X = 4
|
||||
-- Now the definition of Y runs before X (because Freewheeling apps load
|
||||
-- top-level definitions in version order) when X is nil, and Y starts
|
||||
-- raising a confusing error. And all because we modified X. Action at a
|
||||
-- distance.
|
||||
--
|
||||
-- To avoid this we make sure each definition is always self-contained.
|
||||
--
|
||||
-- This feels highly experimental. Concerns:
|
||||
--
|
||||
-- 1. Any side-effects in top-level definitions will run twice. Ugh.
|
||||
-- But then, they will also run any time you restart the app.
|
||||
-- Putting side-effects in the top-level seems like a more unnatural thing
|
||||
-- for a programmer to do than defining one global in terms of another.
|
||||
--
|
||||
-- 2. We still won't catch situations where we depended on a global
|
||||
-- variable and it used to be non-nil but now it's nil:
|
||||
-- define X = 3
|
||||
-- define Y = X
|
||||
-- edit X = 4
|
||||
-- Now Y is nil when it didn't used to be. Which can be confusing. And it
|
||||
-- won't even raise any errors.
|
||||
--
|
||||
-- 3. I'm going to create only a shallow copy of globals. It's hard to
|
||||
-- think about what real, really humongous global variables we might end
|
||||
-- up accidentally copying and turning everything sluggish. We might still
|
||||
-- miss some situation like:
|
||||
-- define io.bar
|
||||
-- define Y = io.bar -- allowed because io.bar has leaked in to the bare environment
|
||||
-- So this isn't perfect.
|
||||
--
|
||||
-- 4. Lua's setfenv is confusing to me. I wonder if there's some situation
|
||||
-- like:
|
||||
-- X = foo()
|
||||
-- that might cause trouble because foo() will continue to use the
|
||||
-- non-initial environment.
|
||||
--
|
||||
-- So far it seems fine. My intent is only to catch order-dependence in
|
||||
-- top-level definitions. The initial environment will have a subset of
|
||||
-- definitions; we prevent the live app from mutating any of them. It seems
|
||||
-- reasonable to assume that any pre-existing functions will not rely on any
|
||||
-- globals some random app is going to introduce.
|
||||
--
|
||||
-- 5. I could probably move the setfenv into eval_in_initial_env, but that
|
||||
-- feels confusing to think about given the table of bar envs is itself
|
||||
-- stored in a global variable.
|
||||
--
|
||||
-- 6. I could probably avoid duplicating eval as eval_in_initial_env, but
|
||||
-- it feels less error-prone to keep the two isolated. We never mess with
|
||||
-- the env of the real eval.
|
||||
setfenv(live.eval_in_initial_env, table.copy(Live.initial_env))
|
||||
local status, err = live.eval_in_initial_env(buf)
|
||||
if not status then
|
||||
live.roll_back()
|
||||
live.send_to_driver('ERROR this definition depends on other globals which can lead to hard-to-debug errors. Please keep top-level definitions order-independent. Define functions for more complex initialization.')
|
||||
return
|
||||
end
|
||||
-- run all tests
|
||||
Test_errors = {}
|
||||
App.run_tests(record_error_by_test)
|
||||
|
@ -363,6 +427,35 @@ function live.full_name(scopes, name)
|
|||
return ns..'.'..name
|
||||
end
|
||||
|
||||
-- exact copy of live.eval that has had setfenv applied to it
|
||||
function live.eval_in_initial_env(buf)
|
||||
local f = load('return '..buf, 'REPL')
|
||||
if f then
|
||||
return pcall(f)
|
||||
end
|
||||
local f, err = load(buf, 'REPL')
|
||||
if f then
|
||||
return pcall(f)
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
function table.copy(h)
|
||||
local result = {}
|
||||
for k,v in pairs(h) do
|
||||
result[k] = v
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function table.length(h)
|
||||
local result = 0
|
||||
for _ in pairs(h) do
|
||||
result = result+1
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- === on error, pause the app and wait for messages
|
||||
|
||||
|
|
Loading…
Reference in New Issue