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) doif not starts_with(k, 'fw_') thentable.insert(core_filenames, k)endendtable.sort(core_filenames)for _,core in ipairs(core_filenames) dolocal filename = ('%04d'):format(manifest[core])..'-'..corelocal f = io.open(filename)if f thenprint(f:read('*a'))print('')endendendfunction starts_with(s, prefix)if #s < #prefix thenreturn falseendfor i=1,#prefix doif s:sub(i,i) ~= prefix:sub(i,i) thenreturn falseendendreturn trueendmain(arg)
# The on-disk representation of freewheeling appsWhen you start up a freewheeling app, you'll see a directory printed out inthe 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)), newdefinitions will go into this directory. Let's call it `$SAVE_DIR` in the restof 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 withothers, and it's also helpful if the driver crashes on your app. Movingdefinitions 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 thisfile, you'll see a JSON table from definition names to version ids. Forexample:```{ "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 streamlinethings:```lua tools/stitch-live.lua 0478-fwmanifest````stitch-live.lua` takes a manifest file as its argument, and prints out allthe definitions that make up the app at that version.To compare two versions of the app, use `stitch-live.lua` to copy thedefinitions 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 arealways disjoint between the two.- This doesn't apply yet to live updates. Two forks of a single live appwill 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 + 1local head_string = love.filesystem.read('head')app.Head = tonumber(head_string)if app.Head > 0 thenapp.Manifest = json.decode(love.filesystem.read(app.versioned_manifest(app.Head)))endapp.load_everything_in_manifest()endfunction app.append_files_with_numeric_prefix(dir, files)for _,file in ipairs(love.filesystem.getDirectoryItems(dir)) dotable.insert(files, file)endendfunction app.check_integrity(files)local manifest_found, file_found = false, falselocal expected_index = 1for _,file in ipairs(files) dofor numeric_prefix, root in file:gmatch('(%d+)-(.+)') do-- only runs oncelocal index = tonumber(numeric_prefix)-- skip files without numeric prefixesif index ~= nil thenif index < expected_index thenprint(index, expected_index)endassert(index >= expected_index)if index > expected_index thenassert(index == expected_index+1)assert(manifest_found and file_found)expected_index = indexmanifest_found, file_found = false, falseendassert(index == expected_index)if root == 'fwmanifest' thenassert(not manifest_found)manifest_found = trueelseassert(not file_found)file_found = trueend
-- if necessary, copy files from repo to save dirif io.open(love.filesystem.getSaveDirectory()..'/0000-freewheeling-start') == nil thenprint('copying all definitions from repo to save dir')for _,filename in ipairs(love.filesystem.getDirectoryItems('')) dofor numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do-- only runs oncelocal buf = love.filesystem.read(filename)print('copying', filename)love.filesystem.write(filename, buf)
endfunction app.load_history(files)local result = {}for _,file in ipairs(files) dofor numeric_prefix, root in file:gmatch('(%d+)-(.+)') do
-- load files from save dirfor _,filename in ipairs(love.filesystem.getDirectoryItems('')) dofor numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do
local index = tonumber(numeric_prefix)-- skipif index ~= nil thenif root ~= 'fwmanifest' thenassert(index == #result+1)table.insert(result, root)end
if tonumber(numeric_prefix) > 0 then -- skip 0000app.Filename[root] = filenametable.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 wellfunction app.load_everything_in_manifest()local files_to_load = {}for k,v in pairs(app.Manifest) doif not starts_with(k, 'fw_') thenlocal root, index = k, vlocal filename = app.versioned_filename(index, root)table.insert(files_to_load, filename)endendtable.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] = nilapp.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] thenlocal 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) doprint(i, v)endapp.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] = nilend
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_versionapp.roll_forward()
end-- eval succeeded without errors; persist the definitionlocal filename = app.Filename[definition_name]if filename == nil thenapp.Final_prefix = app.Final_prefix+1filename = ('%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.Headlocal manifest_filename = app.versioned_manifest(app.Next_version)love.filesystem.write(manifest_filename, json.encode(app.Manifest))app.Head = app.Next_versionlove.filesystem.write('head', tostring(app.Head))app.Next_version = app.Next_version + 1end-- update app.Head and reload app.Manifest appropriatelyfunction 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) doif v == x thenreturn kendend
* This app encourages a style of development that requires top-leveldefinitions to be decoupled from each other. No live functions load untilall definitions have been run. However top-level globals are initialized asthey'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 currentlyhave any checks for this.
This file contains no definition, but is used as a marker in the save dir toindicate all definitions have been copied from the repo to the save dir.