qwe 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/DOCS.md +469 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +22 -0
- data/exe/qwe +118 -0
- data/lib/guerilla.rb +83 -0
- data/lib/puma/plugin/qwe.rb +80 -0
- data/lib/qwe/attribute.rb +193 -0
- data/lib/qwe/db/commits_file.rb +71 -0
- data/lib/qwe/db/record.rb +179 -0
- data/lib/qwe/db/server.rb +211 -0
- data/lib/qwe/db/worker.rb +121 -0
- data/lib/qwe/db.rb +61 -0
- data/lib/qwe/function.rb +98 -0
- data/lib/qwe/mixins/process.rb +14 -0
- data/lib/qwe/mixins/root.rb +45 -0
- data/lib/qwe/mixins/thing.rb +36 -0
- data/lib/qwe/mixins.rb +17 -0
- data/lib/qwe/version.rb +5 -0
- data/lib/qwe.rb +49 -0
- data/lib/spawn_worker.rb +5 -0
- data/qwe.gemspec +32 -0
- metadata +98 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qwe::DB
|
4
|
+
class Record
|
5
|
+
include DRb::DRbUndumped
|
6
|
+
|
7
|
+
def _dump(...)
|
8
|
+
""
|
9
|
+
end
|
10
|
+
|
11
|
+
def self._load(...)
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def detach_at
|
16
|
+
@detach_at || keep
|
17
|
+
end
|
18
|
+
|
19
|
+
def keep
|
20
|
+
@detach_at = Time.now + Worker.instance.detach_timeout
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_detach?
|
24
|
+
detach_at < Time.now
|
25
|
+
end
|
26
|
+
|
27
|
+
def object=(obj)
|
28
|
+
raise "Object for record #{id} is already set" if object
|
29
|
+
@object = obj
|
30
|
+
end
|
31
|
+
|
32
|
+
def id=(id)
|
33
|
+
raise "Object for record #{id} is already set" if self.id
|
34
|
+
@id = id
|
35
|
+
end
|
36
|
+
|
37
|
+
def commit(str)
|
38
|
+
@meta["commits_length"] += str.count("\n")
|
39
|
+
commits_file.write(str)
|
40
|
+
unless str[-1] == "\n"
|
41
|
+
commits_file.write("\n")
|
42
|
+
@meta["commits_length"] += 1
|
43
|
+
end
|
44
|
+
self
|
45
|
+
rescue => e
|
46
|
+
log "Can't write commit #{str}: #{e.full_message}"
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def commits_length=(v)
|
51
|
+
@meta["commits_length"] = v
|
52
|
+
end
|
53
|
+
|
54
|
+
def commits_length
|
55
|
+
@meta["commits_length"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def meta_file
|
59
|
+
File.join(@dir, "meta")
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :dir, :object, :id, :meta
|
63
|
+
|
64
|
+
def commits_file
|
65
|
+
@commits_file ||= CommitsFile.new(@dir)
|
66
|
+
end
|
67
|
+
|
68
|
+
def dump
|
69
|
+
Marshal.dump(object)
|
70
|
+
end
|
71
|
+
|
72
|
+
def dump_object
|
73
|
+
File.binwrite(File.join(dir, commits_length.to_s), dump)
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_object(klass)
|
77
|
+
self.object = klass.new
|
78
|
+
object.init if klass.include?(Qwe::Mixins::Root)
|
79
|
+
dump_object
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(id, server_dir, create: nil, **args)
|
83
|
+
@dir = File.join(server_dir, "records", id.to_s)
|
84
|
+
self.id = id
|
85
|
+
|
86
|
+
if create
|
87
|
+
Qwe::DB::Server.mkdir(dir)
|
88
|
+
@meta = args
|
89
|
+
@meta["commits_length"] = 0
|
90
|
+
File.write(meta_file, JSON.generate(args))
|
91
|
+
if create.is_a?(Class)
|
92
|
+
create_object(create)
|
93
|
+
elsif create.is_a?(Symbol)
|
94
|
+
create_object(Object.module_eval(create.to_s))
|
95
|
+
elsif create.is_a?(String)
|
96
|
+
self.object = Marshal.load(create)
|
97
|
+
File.binwrite(File.join(dir, "0"), create)
|
98
|
+
else
|
99
|
+
self.object = create
|
100
|
+
dump_object
|
101
|
+
end
|
102
|
+
else
|
103
|
+
@meta = JSON.parse(File.read(meta_file))
|
104
|
+
self.object = Marshal.load(File.binread(File.join(dir, "dump")))
|
105
|
+
end
|
106
|
+
|
107
|
+
if object.respond_to?(:record=)
|
108
|
+
object.record = self
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def save
|
113
|
+
save!
|
114
|
+
rescue => e
|
115
|
+
log "Can't save record #{id}: #{e.full_message} #{e.backtrace}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def save!
|
119
|
+
commits_file.close
|
120
|
+
File.binwrite(File.join(dir, "dump"), dump)
|
121
|
+
File.write(meta_file, JSON.generate(@meta))
|
122
|
+
end
|
123
|
+
|
124
|
+
def commits(commit_id = nil)
|
125
|
+
commits_file.read(commit_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
def object_at(commit_id)
|
129
|
+
o = Marshal.load File.read(File.join(dir, "0"))
|
130
|
+
o.instance_eval commits(commit_id)
|
131
|
+
o
|
132
|
+
end
|
133
|
+
|
134
|
+
def detach
|
135
|
+
Worker.instance.detach(id, self)
|
136
|
+
end
|
137
|
+
|
138
|
+
def fork(commit_id = nil)
|
139
|
+
log "Fork at commit id #{commit_id}"
|
140
|
+
commit_id ||= commits_length
|
141
|
+
|
142
|
+
zero = File.binread(File.join(dir, "0"))
|
143
|
+
rec = Worker.instance.allocate_record(zero)
|
144
|
+
|
145
|
+
commits = commits(commit_id)
|
146
|
+
rec.commits_length = commit_id + 1
|
147
|
+
|
148
|
+
begin
|
149
|
+
rec.object.instance_eval(commits)
|
150
|
+
rescue ScriptError, StandardError => e
|
151
|
+
log e.full_message
|
152
|
+
log "Commits are:\n\n#{commits}\n\n"
|
153
|
+
end
|
154
|
+
|
155
|
+
if rec.commits.length > 0
|
156
|
+
log "Commits should not produce another commits:", "\n" + rec.commits
|
157
|
+
raise "Commits loop detected"
|
158
|
+
end
|
159
|
+
|
160
|
+
rec.commits_file.write(commits)
|
161
|
+
rec
|
162
|
+
end
|
163
|
+
|
164
|
+
def obj_eval(rb)
|
165
|
+
object.instance_eval(rb)
|
166
|
+
end
|
167
|
+
|
168
|
+
def archive
|
169
|
+
archive!
|
170
|
+
rescue => e
|
171
|
+
log "Error archiving #{id}: #{e.full_message}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def archive!
|
175
|
+
log "Archive record #{id}"
|
176
|
+
commits_file.archive!
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "etc"
|
4
|
+
|
5
|
+
module Qwe::DB
|
6
|
+
class Server
|
7
|
+
include DRb::DRbUndumped
|
8
|
+
include Qwe::Mixins::Process
|
9
|
+
|
10
|
+
DEFAULT_PORT = 3228
|
11
|
+
DEFAULT_HOST = "druby://localhost"
|
12
|
+
|
13
|
+
attr_reader :uri, :dir, :host, :port, :records_length, :threads_count
|
14
|
+
|
15
|
+
def self.default_uri
|
16
|
+
"#{DEFAULT_HOST}:#{DEFAULT_PORT}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(
|
20
|
+
dir: "#{Dir.pwd}/qwe_db",
|
21
|
+
host: DEFAULT_HOST,
|
22
|
+
port: DEFAULT_PORT,
|
23
|
+
require_file: nil,
|
24
|
+
no_jit: false,
|
25
|
+
threads: Etc.nprocessors,
|
26
|
+
gc_interval: 0,
|
27
|
+
detach_timeout: 300
|
28
|
+
)
|
29
|
+
@port = port
|
30
|
+
@host = host
|
31
|
+
@dir = dir
|
32
|
+
@uri = "#{@host}:#{@port}"
|
33
|
+
@threads_count = threads
|
34
|
+
|
35
|
+
@worker_args = [RbConfig.ruby]
|
36
|
+
@worker_args.push "--jit" unless no_jit
|
37
|
+
@worker_args += ["#{__dir__}/../../spawn_worker.rb", uri, dir, gc_interval.to_s, detach_timeout.to_s]
|
38
|
+
@worker_args.push require_file if require_file
|
39
|
+
|
40
|
+
@workers = {}
|
41
|
+
@records = {}
|
42
|
+
|
43
|
+
log "Initialized server in #{dir}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def start
|
47
|
+
self.class.mkdir(dir)
|
48
|
+
d = File.join(dir, "records")
|
49
|
+
self.class.mkdir(d)
|
50
|
+
@records_length = (Dir.new(d).children.map(&:to_i).max || 0) + 1
|
51
|
+
@drb = DRb.start_service(uri, self)
|
52
|
+
|
53
|
+
trap_stop_signals
|
54
|
+
|
55
|
+
log "Started server at #{@drb.uri}, records_length = #{records_length}, pid = #{Process.pid}"
|
56
|
+
|
57
|
+
@threads_count.times do
|
58
|
+
spawn_worker(wait: false)
|
59
|
+
end
|
60
|
+
|
61
|
+
@ready = true
|
62
|
+
|
63
|
+
@drb.thread.join
|
64
|
+
end
|
65
|
+
|
66
|
+
def ready?
|
67
|
+
@ready || false
|
68
|
+
end
|
69
|
+
|
70
|
+
def [](id)
|
71
|
+
id = id.to_i
|
72
|
+
w = @records[id]
|
73
|
+
if w
|
74
|
+
if w.is_a?(Thread)
|
75
|
+
w.value
|
76
|
+
else
|
77
|
+
w[id]
|
78
|
+
end
|
79
|
+
else
|
80
|
+
load(id)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def workers_length
|
85
|
+
@workers.length
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.mkdir(d)
|
89
|
+
Dir.mkdir(d) unless Dir.exist?(d)
|
90
|
+
end
|
91
|
+
|
92
|
+
def records_length=(len)
|
93
|
+
@records_length = len
|
94
|
+
File.write(rl_file, len)
|
95
|
+
end
|
96
|
+
|
97
|
+
def stop
|
98
|
+
return if @stopping
|
99
|
+
@stopping = true
|
100
|
+
log "Stopping"
|
101
|
+
@workers.each do |pid, worker|
|
102
|
+
log "Terminate worker #{pid}"
|
103
|
+
Process.kill("TERM", pid)
|
104
|
+
rescue Errno::ESRCH
|
105
|
+
log "Worker #{pid} is already dead"
|
106
|
+
rescue => e
|
107
|
+
log "Error terminating worker #{pid}: #{e.full_message}"
|
108
|
+
end
|
109
|
+
exit
|
110
|
+
end
|
111
|
+
|
112
|
+
SPAWN_WORKER_TIMEOUT = 50
|
113
|
+
|
114
|
+
def spawn_worker(wait: true)
|
115
|
+
pid = spawn(*@worker_args)
|
116
|
+
Process.detach(pid)
|
117
|
+
|
118
|
+
@workers[pid] = Thread.new do
|
119
|
+
sleep SPAWN_WORKER_TIMEOUT
|
120
|
+
raise "Worker did not start after #{SPAWN_WORKER_TIMEOUT}s timeout"
|
121
|
+
end
|
122
|
+
@workers[pid].join if wait
|
123
|
+
pid
|
124
|
+
end
|
125
|
+
|
126
|
+
def pick_worker(attempt = 0)
|
127
|
+
pid = @workers.keys.sample
|
128
|
+
if @workers[pid].is_a? Thread
|
129
|
+
@workers[pid].join
|
130
|
+
end
|
131
|
+
@workers[pid]
|
132
|
+
rescue => e
|
133
|
+
log "Error picking worker: #{e.full_message}, attempt #{attempt}"
|
134
|
+
raise "Give up" if attempt > 2
|
135
|
+
del_worker(pid)
|
136
|
+
spawn_worker
|
137
|
+
pick_worker(attempt + 1)
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_worker_uri(uri, pid)
|
141
|
+
t = @workers[pid]
|
142
|
+
@workers[pid] = DRbObject.new_with_uri(uri)
|
143
|
+
log "Worker #{pid} is up"
|
144
|
+
t.exit
|
145
|
+
end
|
146
|
+
|
147
|
+
def crash_worker(pid, message)
|
148
|
+
@workers[pid].raise message
|
149
|
+
@workers.delete(pid)
|
150
|
+
end
|
151
|
+
|
152
|
+
def del_worker(pid)
|
153
|
+
if @workers[pid].is_a?(Thread)
|
154
|
+
@workers[pid].exit
|
155
|
+
end
|
156
|
+
@workers.delete(pid)
|
157
|
+
end
|
158
|
+
|
159
|
+
def create(klass)
|
160
|
+
id = records_length
|
161
|
+
w = pick_worker
|
162
|
+
w.create(id, klass)
|
163
|
+
log "Create record #{id} in worker #{w.uri}"
|
164
|
+
@records[id] = w
|
165
|
+
@records_length += 1
|
166
|
+
id
|
167
|
+
end
|
168
|
+
|
169
|
+
def detach(id)
|
170
|
+
@records[id].detach(id)
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
def detach_record(id)
|
175
|
+
@records.delete(id)
|
176
|
+
end
|
177
|
+
|
178
|
+
def load(id)
|
179
|
+
@records[id] = Thread.new do
|
180
|
+
w = pick_worker
|
181
|
+
log "Load record #{id} in worker #{w.uri}"
|
182
|
+
obj = w.load(id)
|
183
|
+
@records[id] = w
|
184
|
+
obj
|
185
|
+
end
|
186
|
+
@records[id].value
|
187
|
+
end
|
188
|
+
|
189
|
+
def allocate_record(pid)
|
190
|
+
id = records_length
|
191
|
+
@records_length += 1
|
192
|
+
@records[id] = @workers[pid]
|
193
|
+
log "Allocate record #{id}"
|
194
|
+
id
|
195
|
+
end
|
196
|
+
|
197
|
+
def destroy(id)
|
198
|
+
destroy!(id)
|
199
|
+
rescue => e
|
200
|
+
log "Can't destroy - #{e.full_message}"
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def destroy!(id)
|
205
|
+
throw "Record #{id} is in use" if @records[id]
|
206
|
+
FileUtils.rm_r(File.join(dir, "records", id.to_s))
|
207
|
+
log "Destroyed #{id}"
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qwe::DB
|
4
|
+
class Worker
|
5
|
+
include DRb::DRbUndumped
|
6
|
+
include Qwe::Mixins::Process
|
7
|
+
|
8
|
+
attr_reader :server_dir, :server, :records, :gc_interval, :detach_timeout, :requirements
|
9
|
+
|
10
|
+
POLL_INTERVAL = 20
|
11
|
+
|
12
|
+
def initialize(server_url, server_dir, gc_interval = 0, detach_timeout = 300, requirements = nil)
|
13
|
+
@server_dir = server_dir
|
14
|
+
@server = DRbObject.new_with_uri(server_url)
|
15
|
+
@gc_interval = gc_interval.to_i
|
16
|
+
@detach_timeout = detach_timeout.to_i
|
17
|
+
|
18
|
+
@requirements = requirements
|
19
|
+
begin
|
20
|
+
require requirements if requirements
|
21
|
+
rescue ScriptError, StandardError => e
|
22
|
+
server.crash_worker(Process.pid, "Error requiring '#{requirements}': #{e.full_message}")
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
Qwe::Function.compile_all
|
27
|
+
|
28
|
+
@records = {}
|
29
|
+
@object_refs = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def start_poll
|
33
|
+
@poll_thread = Thread.new do
|
34
|
+
loop do
|
35
|
+
sleep POLL_INTERVAL
|
36
|
+
@records.keys.each do |id|
|
37
|
+
r = @records[id]
|
38
|
+
detach(id, r) if r.should_detach?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def detach(id, record = nil)
|
45
|
+
record ||= @records[id]
|
46
|
+
@server.detach_record(id)
|
47
|
+
@object_refs.delete(id)
|
48
|
+
@records.delete(id)
|
49
|
+
record.save
|
50
|
+
log "Detach record #{id} from worker #{uri}"
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def start_gc
|
55
|
+
@gc_thread = Thread.new do
|
56
|
+
loop do
|
57
|
+
sleep @gc_interval
|
58
|
+
next if @records.empty?
|
59
|
+
GC.start
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def uri
|
65
|
+
@drb&.uri
|
66
|
+
end
|
67
|
+
|
68
|
+
def work
|
69
|
+
@drb = DRb.start_service("druby://localhost:0", self)
|
70
|
+
log "Started worker at #{@drb.uri}, pid = #{Process.pid}, requirements = #{@requirements}"
|
71
|
+
@server.set_worker_uri(uri, Process.pid)
|
72
|
+
|
73
|
+
start_poll
|
74
|
+
start_gc if @gc_interval > 0
|
75
|
+
|
76
|
+
trap_stop_signals
|
77
|
+
|
78
|
+
@drb.thread.join
|
79
|
+
end
|
80
|
+
|
81
|
+
def create(id, klass)
|
82
|
+
@records[id] = Qwe::DB::Record.new(id, server_dir, create: klass)
|
83
|
+
end
|
84
|
+
|
85
|
+
def load(id)
|
86
|
+
@records[id] = Qwe::DB::Record.new(id, server_dir)
|
87
|
+
DRbObject.new(@records[id].object)
|
88
|
+
end
|
89
|
+
|
90
|
+
def [](i)
|
91
|
+
@object_refs[i] ||= DRbObject.new(@records[i].object)
|
92
|
+
end
|
93
|
+
|
94
|
+
def stop
|
95
|
+
return if @stopping
|
96
|
+
@stopping = true
|
97
|
+
log "Stopping"
|
98
|
+
@records.keys.each { |id| @records[id].save }
|
99
|
+
exit
|
100
|
+
end
|
101
|
+
|
102
|
+
def allocate_record(zero)
|
103
|
+
log "Allocate record for #{zero.class}"
|
104
|
+
id = server.allocate_record(Process.pid)
|
105
|
+
records[id] = Qwe::DB::Record.new(id, server_dir, create: zero)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.instance
|
109
|
+
@@instance
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.spawn(*)
|
113
|
+
@@instance = Qwe::DB::Worker.new(*)
|
114
|
+
@@instance.work
|
115
|
+
end
|
116
|
+
|
117
|
+
def e(str)
|
118
|
+
instance_eval str
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/qwe/db.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "db/commits_file"
|
4
|
+
require_relative "db/server"
|
5
|
+
require_relative "db/worker"
|
6
|
+
require_relative "db/record"
|
7
|
+
|
8
|
+
module Qwe
|
9
|
+
module DB
|
10
|
+
# Server #
|
11
|
+
|
12
|
+
def self.serve(*a1, **a2)
|
13
|
+
@@srv = Server.new(*a1, **a2)
|
14
|
+
@@srv.start
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.stop
|
18
|
+
@@srv.stop
|
19
|
+
end
|
20
|
+
|
21
|
+
# Client #
|
22
|
+
|
23
|
+
def self.connect(uri = Server.default_uri)
|
24
|
+
@@server = DRbObject.new_with_uri(uri)
|
25
|
+
300.times do
|
26
|
+
if server.ready?
|
27
|
+
return server
|
28
|
+
end
|
29
|
+
sleep 0.1
|
30
|
+
rescue
|
31
|
+
sleep 0.1
|
32
|
+
end
|
33
|
+
raise "Couldn't connect to server in more than 30s"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.server
|
37
|
+
@@server ||= DRbObject.new_with_uri(Server.default_uri)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.[](id)
|
41
|
+
server[id]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.create(...)
|
45
|
+
server.create(...)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.destroy(...)
|
49
|
+
server.destroy(...)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.destroy!(...)
|
53
|
+
server.destroy!(...)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.detach(id)
|
57
|
+
server.detach(id)
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/qwe/function.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qwe
|
4
|
+
class Function
|
5
|
+
attr_accessor :klass
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def name=(n)
|
9
|
+
@name = n.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.instances
|
13
|
+
@@instances ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.compile_all
|
17
|
+
instances.each(&:compile)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(klass, name)
|
21
|
+
self.klass = klass
|
22
|
+
self.name = name
|
23
|
+
self.class.instances.push(self)
|
24
|
+
Qwe[klass, :functions, name] = self
|
25
|
+
|
26
|
+
unless klass.respond_to?(:compile)
|
27
|
+
klass.define_singleton_method(:compile) do
|
28
|
+
Qwe[self, :functions].each do |n, f|
|
29
|
+
f.compile
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def args
|
36
|
+
@args ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def arg(name, default = nil)
|
40
|
+
args[name] = default
|
41
|
+
end
|
42
|
+
|
43
|
+
def keywords
|
44
|
+
@keywords ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def keyword(name, default = nil)
|
48
|
+
keywords[name] = default
|
49
|
+
end
|
50
|
+
|
51
|
+
def stages
|
52
|
+
@stages ||= []
|
53
|
+
end
|
54
|
+
|
55
|
+
def stage(code)
|
56
|
+
@compiled = false
|
57
|
+
stages.push(code) if code
|
58
|
+
end
|
59
|
+
|
60
|
+
def code
|
61
|
+
rb = "def #{name}(" + \
|
62
|
+
((args.to_a.map { |a| "#{a[0]} = #{a[1].to_rb}" }) + \
|
63
|
+
(keywords.to_a.map { |a| "#{a[0]}: #{a[1].to_rb}" })).join(", ") + \
|
64
|
+
")\n"
|
65
|
+
stages.each do |s|
|
66
|
+
rb += " " + s + "\n"
|
67
|
+
end
|
68
|
+
rb += "end"
|
69
|
+
end
|
70
|
+
|
71
|
+
def compile
|
72
|
+
return if @compiled
|
73
|
+
|
74
|
+
begin
|
75
|
+
klass.module_eval(code)
|
76
|
+
rescue ScriptError, StandardError => e
|
77
|
+
log "Error compiling #{klass}.#{name}, code is: \n#{code}\n"
|
78
|
+
raise e
|
79
|
+
end
|
80
|
+
|
81
|
+
@compiled = true
|
82
|
+
end
|
83
|
+
|
84
|
+
def compiled?
|
85
|
+
@compiled
|
86
|
+
end
|
87
|
+
|
88
|
+
def inspect
|
89
|
+
"\nFunction #{klass}.#{name}\n#{code}\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.compile(klass, name, &block)
|
93
|
+
f = Qwe::Function.new(klass, name)
|
94
|
+
yield f
|
95
|
+
f.compile
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|