aboutsummaryrefslogtreecommitdiffstats
path: root/alv/copilot/base.moon
blob: 2fb0ebc482999bc3126656daabfa44f2d8ed6f4e (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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
----
-- File watcher and runtime entrypoint.
--
-- @classmod Copilot
lfs = require 'lfs'
import Scope from require 'alv.scope'
import FSModule 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={ 'udp-server': false }) ->
  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 table args
  new: (@args={}) =>
    @T = 0
    @last_modification = 0
    @last_modules = {}
    @open @args[1] if @args[1]

    if @args['udp-server']
      import UDPServer from require 'alv.copilot.udp'
      @adapter = UDPServer @

--- 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 = FSModule file
    @active_module = @last_modules.__root

    COPILOT = nil

  --- require a module.
  -- @tparam string name
  -- @tparam Scope scope
  -- @treturn RTNode root
  require: (name, scope) =>
    Error.wrap "loading module '#{name}'", ->
      ok, result = pcall require, "alv-lib.#{name}"
      if ok
        result = RTNode :result unless result.__class == RTNode
        result
      elseif not result\match "'alv%-lib%.[^']+' not found"
        error result
      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 FSModule "#{prefix}#{name}.alv"
          L\trace "entering module #{mod}"
          @modules[name] = mod
          @active_module = mod
          ok, err = pcall mod\eval, scope
          L\trace "returning to module #{mod}"
          @active_module = last
          if ok
            mod.root
          else
            error err

  --- poll for changes and tick.
  tick: =>
    @adapter\tick! if @adapter

    assert not COPILOT, "another Copilot is already running!"
    return unless @last_modules.__root

    COPILOT = @

    ok, err = @poll!
    if not ok
      L\error err

    root = @last_modules.__root
    if root and root.root
      L\set_time 'run'
      ok, error = Error.try "updating", ->
        if root.root\poll_io!
          root.root\tick!
          @T += 1
      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.
  --
  -- @treturn boolean ok
  -- @treturn error err
  poll: =>
    dirty = {}
    for name, mod in pairs @last_modules
      if mod\poll! > @last_modification
        table.insert dirty, mod

    return true if #dirty == 0

    @eval dirty

  --- try to re-evaluate in response to module changes.
  -- @treturn boolean ok
  -- @treturn error err
  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
      return false, err

    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
    true

{
  :parse_args
  :Copilot
}