No more version history, now we have just the contents of the current version.
Editing a definition no longer changes the order in which definitions load.
This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example:
using driver.love, define Foo = 3
in a definition
define Bar = Foo + 1
edit and redefine Foo = 4
Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading.
After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly.
Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git.
I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
QFFTXR7EODYP3OPAO5ZJ4UDLKPBYBU2ESWHP3ABH3BEBYBIONCWAC
OFRGJFGIAGJ4DS7EWP7OLV7RPPZ5KDTAGFL77SZGPMICICO5BIPQC
NWKIHJI5UNTW4BTWE6QP4Q3SGZ375PG6SIKYT5CNUWKYFC7APHMQC
YNQNIR7G6PBKRNX6I26H6T4KVVGBYDFCQPJWIA54OP5RK3OYUQOAC
AJLUO3UIULMWCJXLQ4UDI2ACGAU2S4AWQ4UB2HF4SDDB44EVVFEAC
MOUXJ6VT5YQABAMPBAEABJHGIVQLQ4X7UPBIUVINK4KWD2SWAPXAC
3EW4YU6LLM2FLMKZER7GGKRSWT7JWHPCBL2IHAQNQXLEZA7QJJ6AC
JILSWFACAJYIQIVEFKDRIIMRACV43LCF4SVH45VGLL6QFWRRDCKAC
CCVA7636SZDLUSDVELXB6AGQI5SQCT6BG4YL4XGPHDXJY7EYXKQAC
WUEDWXC4PE4IFLSKC2MQFKEYT3BOUMKK4EUIIYSSBHKLIH3DTI7AC
LKUW6TVDJD66K2QFWWJF3URDGCNYAZ5VIGKIC6EMRDRX5ARICA2QC
EUWHJ7JJ4TY2Q2II4SUYTBVMZJ7WWEN2THAKVWGC35P2R2ENYU4AC
WIBNGKLFGR735FGW6LB72RWOOFFZ6JTLXXSGC7G6HAKKOZVKETMQC
TNRO6KLZXIZUFWKCXSWAJHN2CMHS56ATGGULOKMJC2YNCFRJZKLAC
7ZIFAEBRSUYG3HBD6NIZYKUNAZZ7J65RYHWZVRHNLNKGHCZGCVHAC
UUX7J2OA544YWISPG3A6FJ6GKAS2MLESZZMCZN7KVCKMDVYJDZGQC
5TZBSRBNE2EK4Z7EDMQCE6NMMIBIKKPZMMSPHW6GUDGMJWSPFRTAC
ECUOUZI7JCVMXHY57MF6VA6FQMYOXRZXAMWCYWZVTAQ266UR23MQC
OJLXS67N5327FN6YAZ2DUVXUEZKEN6YE3EJ5CYJP5VALLQLCXREAC
5T6SHEIT24FYUAZHSC27XXY4ISZIH3O4HID37FARWVZD4EGSVMOQC
JIWWTY7RDSOBGAOSU7KBOM2SHEM543FOWIM7GEIW3IZWVJ2EDJVAC
KGILP75EVHHDWV7OY5JONQKWHDGQUQAZJHHMFSG63SFD24Q7GMRAC
OI2U4SQ3DA6X57GWOWCWBHH4VI56QH6PXXFUVPY2ZZZOKE6ZBLFAC
NBRDMLUTRVP2AECEL4UT77TYHCPO4K2X2IAYWY7N5VZ5WQHTFZFAC
json = require 'json'
function main(args)
local infile = io.open(args[1])
local manifest_s = infile:read('*a')
infile:close()
local manifest = json.decode(manifest_s)
local core_filenames = {}
for k,v in pairs(manifest) do
if not starts_with(k, 'fw_') then
table.insert(core_filenames, k)
end
end
table.sort(core_filenames)
for _,core in ipairs(core_filenames) do
local filename = ('%04d'):format(manifest[core])..'-'..core
local f = io.open(filename)
if f then
print(f:read('*a'))
print('')
end
end
end
function starts_with(s, prefix)
if #s < #prefix then
return false
end
for i=1,#prefix do
if s:sub(i,i) ~= prefix:sub(i,i) then
return false
end
end
return true
end
main(arg)
# The on-disk representation of freewheeling apps
When you start up a freewheeling app, you'll see a directory printed out in
the parent terminal (always launch it from a terminal window):
```
new edits will go to /home/...
```
When editing such an app using the driver (see [README.md](README.md)), new
definitions will go into this directory. Let's call it `$SAVE_DIR` in the rest
of this doc.
It is always safe to move such definitions into this repo. (We'll call it `.`
in the rest of this doc.) You'll want to do this if you're sharing them with
others, and it's also helpful if the driver crashes on your app. Moving
definitions will never, ever change app behavior.
```sh
$ mv -i $SAVE_DIR/[0-9]* . # should never clobber any existing files
$ mv $SAVE_DIR/head . # expected to clobber the existing file
```
Try looking inside the `head` file with a text editor. It'll contain a number,
the current version of the _manifest_ for this app. For example:
```
478
```
This means the current state of the app is in a file called `0478-fwmanifest`.
If you moved the files you should see such a file in `.`. If you open this
file, you'll see a JSON table from definition names to version ids. For
example:
```
{ "a": 273, "b": 478}
```
This means the current definition of `a` is in `0273-a` and of `b` in
`0478-b`.
Poking around these files gets repetitive, so there's a tool to streamline
things:
```
lua tools/stitch-live.lua 0478-fwmanifest
```
`stitch-live.lua` takes a manifest file as its argument, and prints out all
the definitions that make up the app at that version.
To compare two versions of the app, use `stitch-live.lua` to copy the
definitions in each into a separate file, and use a file comparison tool (e.g.
`diff`) to compare the two files.
# Scenarios considered in designing this representation
* Capture history of changes.
- Though it is perhaps too fine-grained and noisy.
* Merge changes from non-live forks to live ones.
- New files in repo can't hide changes in save dir, because filenames are
always disjoint between the two.
- This doesn't apply yet to live updates. Two forks of a single live app
will likely have unnecessary merge conflicts.
* No special tools required to publish changes to others.
- Just move files from save dir to repo.
# Scenarios I _would_ like to take into consideration in the future
* Cleaner commits; it's clear what changed.
* merge changes between live forks.
-- on incoming messages to a specific file, however, the app must:
-- save the message's value to a new, smallest unused numeric prefix
-- execute the value
-- if there's an error, go back to the previous value of the same
-- definition if one exists
-- on incoming messages to a specific file, the app must:
-- determine the definition name from the first word
-- execute the value, returning any errors
-- look up the filename for the definition or define a new filename for it
-- save the message's value to the filename
local files = {}
app.append_files_with_numeric_prefix('', files)
table.sort(files)
app.check_integrity(files)
app.append_files_with_numeric_prefix(love.filesystem.getSaveDirectory(), files)
table.sort(files)
app.check_integrity(files)
app.History = app.load_history(files)
app.Next_version = #app.History + 1
local head_string = love.filesystem.read('head')
app.Head = tonumber(head_string)
if app.Head > 0 then
app.Manifest = json.decode(love.filesystem.read(app.versioned_manifest(app.Head)))
end
app.load_everything_in_manifest()
end
function app.append_files_with_numeric_prefix(dir, files)
for _,file in ipairs(love.filesystem.getDirectoryItems(dir)) do
table.insert(files, file)
end
end
function app.check_integrity(files)
local manifest_found, file_found = false, false
local expected_index = 1
for _,file in ipairs(files) do
for numeric_prefix, root in file:gmatch('(%d+)-(.+)') do
-- only runs once
local index = tonumber(numeric_prefix)
-- skip files without numeric prefixes
if index ~= nil then
if index < expected_index then
print(index, expected_index)
end
assert(index >= expected_index)
if index > expected_index then
assert(index == expected_index+1)
assert(manifest_found and file_found)
expected_index = index
manifest_found, file_found = false, false
end
assert(index == expected_index)
if root == 'fwmanifest' then
assert(not manifest_found)
manifest_found = true
else
assert(not file_found)
file_found = true
end
-- if necessary, copy files from repo to save dir
if io.open(love.filesystem.getSaveDirectory()..'/0000-freewheeling-start') == nil then
print('copying all definitions from repo to save dir')
for _,filename in ipairs(love.filesystem.getDirectoryItems('')) do
for numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do
-- only runs once
local buf = love.filesystem.read(filename)
print('copying', filename)
love.filesystem.write(filename, buf)
end
function app.load_history(files)
local result = {}
for _,file in ipairs(files) do
for numeric_prefix, root in file:gmatch('(%d+)-(.+)') do
-- load files from save dir
for _,filename in ipairs(love.filesystem.getDirectoryItems('')) do
for numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do
local index = tonumber(numeric_prefix)
-- skip
if index ~= nil then
if root ~= 'fwmanifest' then
assert(index == #result+1)
table.insert(result, root)
end
if tonumber(numeric_prefix) > 0 then -- skip 0000
app.Filename[root] = filename
table.insert(app.Filenames_to_load, filename)
app.Final_prefix = math.max(app.Final_prefix, tonumber(numeric_prefix))
-- return any error encountered, or nil if all well
function app.load_everything_in_manifest()
local files_to_load = {}
for k,v in pairs(app.Manifest) do
if not starts_with(k, 'fw_') then
local root, index = k, v
local filename = app.versioned_filename(index, root)
table.insert(files_to_load, filename)
end
end
table.sort(files_to_load)
for _,filename in ipairs(files_to_load) do
function app.load_all()
for _,filename in ipairs(app.Filenames_to_load) do
--? print('loading', filename)
app.Manifest[APP] = love.filesystem.getIdentity() -- doesn't need to be persisted, but no harm if it does..
app.send(json.encode(app.Manifest))
app.Filename[APP] = love.filesystem.getIdentity()
app.send(json.encode(app.Filename))
local definition_name = buf:match('^%S+%s+(%S+)')
app.Manifest[definition_name] = nil
app.eval(definition_name..' = nil') -- ignore errors which will likely be from keywords like `function = nil`
local next_filename = app.versioned_filename(app.Next_version, definition_name)
love.filesystem.write(next_filename, '')
table.insert(app.History, definition_name)
app.roll_forward()
local definition_name = buf:match('^%s*%S+%s+(%S+)')
if app.Filename[definition_name] then
local index = table.find(app.Filenames_to_load, app.Filename[definition_name])
print('index')
table.remove(app.Filenames_to_load, index)
for i,v in ipairs(app.Filenames_to_load) do
print(i, v)
end
app.eval(definition_name..' = nil') -- ignore errors which will likely be from keywords like `function = nil`
print(_G[definition_name])
love.filesystem.remove(app.Filename[definition_name])
print('deleted')
app.Filename[definition_name] = nil
end
local next_filename = app.versioned_filename(app.Next_version, definition_name)
love.filesystem.write(next_filename, buf)
table.insert(app.History, definition_name)
app.Manifest[definition_name] = app.Next_version
app.roll_forward()
end
-- eval succeeded without errors; persist the definition
local filename = app.Filename[definition_name]
if filename == nil then
app.Final_prefix = app.Final_prefix+1
filename = ('%04d-%s'):format(app.Final_prefix, definition_name)
table.insert(app.Filenames_to_load, filename)
app.Filename[definition_name] = filename
-- update app.Head and record the new app.Manifest (which caller has already modified)
function app.roll_forward()
app.Manifest[PARENT] = app.Head
local manifest_filename = app.versioned_manifest(app.Next_version)
love.filesystem.write(manifest_filename, json.encode(app.Manifest))
app.Head = app.Next_version
love.filesystem.write('head', tostring(app.Head))
app.Next_version = app.Next_version + 1
end
-- update app.Head and reload app.Manifest appropriately
function app.roll_back()
app.Head = app.Manifest[PARENT]
love.filesystem.write('head', tostring(app.Head))
local previous_manifest_filename = app.versioned_manifest(app.Head)
app.Manifest = json.decode(love.filesystem.read(previous_manifest_filename))
function table.find(h, x)
for k,v in pairs(h) do
if v == x then
return k
end
end
* This app encourages a style of development that requires top-level
definitions to be decoupled from each other. No live functions load until
all definitions have been run. However top-level globals are initialized as
they're loaded. This makes a definition like this a very bad idea, assuming
`Foo` and `Bar` are top-level variables:
Foo = {
Bar
}
Don't assume that Bar will exist when Foo is defined. We don't currently
have any checks for this.
This file contains no definition, but is used as a marker in the save dir to
indicate all definitions have been copied from the repo to the save dir.