aboutsummaryrefslogtreecommitdiffstats
path: root/alv/copilot/base.moon
blob: 5d752a656ff056687a25ce3594e984a7f9345719 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
----
-- File watcher and runtime entrypoint.
--
-- @classmod Copilot
lfs = require 'lfs'
import Scope from require 'alv.scope'
import Module from require 'alv.module'
import Error from require 'alv.error'
import RTNode from require 'alv.rtnode'
import Constant from require 'alv.result'

parse_args = (args, out={}) ->
  local key
  for a in *args
    if key
      out[key] = a
      key = nil
    else if match = a\match '^%-%-(.*)'
      if 'boolean' == type out[match]
        out[match] = true
      else
        key = match
    else
      table.insert out, a
  assert not key, "value for option '--#{key}' missing!"
  out

export COPILOT

class Copilot
--- static functions
-- @section static

  --- create a new Copilot.
  -- @classmethod
  -- @tparam string file name/path of the alive file to watch and execute
  new: (file) =>
    @T = 0
    @last_modification = 0
    @last_modules = {}
    @open file if file

--- members
-- @section members

  --- current tick
  -- @tfield number T

  --- change the running script.
  -- @tparam string file
  open: (file) =>
    assert not COPILOT, "another Copilot is already running!"
    COPILOT = @

    if old = @last_modules.__root
      old\destroy!

    @active_modules = nil
    @last_modules.__root = nil

    @last_modules.__root = Module file
    @active_module = @last_modules.__root

    COPILOT = nil

  --- require a module.
  -- @tparam string name
  -- @treturn RTNode root
  require: (name) =>
    Error.wrap "loading module '#{name}'", ->
      ok, lua = pcall require, "alv-lib.#{name}"
      if ok
        RTNode result: Constant.wrap lua
      elseif not lua\match "not found"
        error lua
      else
        assert @modules, "no current eval cycle?"
        if mod = @modules[name]
          mod.root\make_ref!
        else
          last = @active_module
          prefix = if b = last.file\match'(.*)/[^/]*$' then b .. '/' else ''
          mod = @last_modules[name] or Module "#{prefix}#{name}.alv"
          L\trace "entering module #{mod}"
          @modules[name] = mod
          @active_module = mod
          ok, err = pcall mod\eval
          L\trace "returning to module #{mod}"
          @active_module = last
          if ok
            mod.root
          else
            error err

  --- poll for changes and tick.
  tick: =>
    assert not COPILOT, "another Copilot is already running!"
    return unless @last_modules.__root

    COPILOT = @
    @T += 1

    @poll!

    root = @last_modules.__root
    if root and root.root
      L\set_time 'run'
      ok, error = Error.try "updating", ->
        root.root\poll_io!
        root.root\tick!
      if not ok
        L\print error

    COPILOT = nil

  --- poll all loaded modules for changes.
  --
  -- Call `eval` if there are any, and write changed and newly added modules
  -- back to disk.
  poll: =>
    dirty = {}
    for name, mod in pairs @last_modules
      if mod\poll! > @last_modification
        table.insert dirty, mod

    return if #dirty == 0

    @eval dirty

  --- try to re-evaluate in response to module changes.
  eval: (dirty) =>
    @last_modification = os.time!
    L\set_time 'eval'
    L\print "changes to files: #{table.concat [m.file for m in *dirty], ', '}"

    @modules = { __root: @last_modules.__root }
    ok, err = Error.try "processing changes", @modules.__root\eval

    if not ok
      for name, mod in pairs @modules
        mod\rollback!
      @modules = nil
      L\error err
      return

    for name, mod in pairs @last_modules
      if not @modules[name]
        mod\destroy!

    for name, mod in pairs @modules
      mod\finish!

    @last_modification = os.time!
    @last_modules, @modules = @modules, nil

{
  :parse_args
  :Copilot
}