Skip to content

Commit b31d493

Browse files
author
KDr2
committed
tape instruction support
1 parent 375e2f8 commit b31d493

File tree

4 files changed

+178
-49
lines changed

4 files changed

+178
-49
lines changed

src/tapedfunction.jl

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
1-
mutable struct Instruction{F}
1+
abstract type AbstractInstruction end
2+
3+
mutable struct Tape
4+
tape::Vector{AbstractInstruction}
5+
counter::Int
6+
owner
7+
end
8+
9+
mutable struct Instruction{F} <: AbstractInstruction
210
fun::F
311
input::Tuple
412
output
5-
tape
13+
tape::Tape
614
end
715

8-
9-
mutable struct Tape
10-
tape::Vector{Instruction}
11-
owner
16+
mutable struct TapeInstruction <: AbstractInstruction
17+
subtape::Tape
18+
tape::Tape
1219
end
1320

14-
Tape() = Tape(Vector{Instruction}(), nothing)
15-
Tape(owner) = Tape(Vector{Instruction}(), owner)
21+
Tape() = Tape(Vector{AbstractInstruction}(), 1, nothing)
22+
Tape(owner) = Tape(Vector{AbstractInstruction}(), 1, owner)
1623
MacroTools.@forward Tape.tape Base.iterate, Base.length
1724
MacroTools.@forward Tape.tape Base.push!, Base.getindex, Base.lastindex
1825
const NULL_TAPE = Tape()
1926

27+
function setowner!(tape::Tape, owner)
28+
tape.owner = owner
29+
for ins in tape
30+
isa(ins, TapeInstruction) && setowner!(ins.subtape, owner)
31+
end
32+
end
33+
2034
mutable struct Box{T}
2135
val::T
2236
end
2337

2438
val(x) = x
2539
val(x::Box) = x.val
2640
box(x) = Box(x)
41+
box(x::Box) = x
2742
any_box(x) = Box{Any}(x)
43+
any_box(x::Box) = Box{Any}(x.val)
2844

2945
gettape(x) = nothing
3046
gettape(x::Instruction) = x.tape
@@ -45,6 +61,12 @@ function Base.show(io::IO, instruction::Instruction)
4561
println(io, "Instruction($(fun)$(map(val, instruction.input)), tape=$(objectid(tape)))")
4662
end
4763

64+
function Base.show(io::IO, ti::TapeInstruction)
65+
subtape = ti.subtape
66+
tape = ti.tape
67+
println(io, "TapeInstruction($(subtape)), tape=$(objectid(tape)))")
68+
end
69+
4870
function Base.show(io::IO, tp::Tape)
4971
buf = IOBuffer()
5072
print(buf, "$(length(tp))-element Tape")
@@ -63,25 +85,73 @@ function (instr::Instruction{F})() where F
6385
instr.output.val = output
6486
end
6587

88+
function (instr::TapeInstruction)()
89+
run(instr.subtape)
90+
end
91+
6692
function run(tape::Tape, args...)
67-
input = map(box, args)
68-
tape[1].input = input
93+
if length(args) > 0
94+
input = map(box, args)
95+
tape[1].input = input
96+
end
6997
for instruction in tape
7098
instruction()
99+
tape.counter += 1
71100
end
72101
end
73102

103+
# if we should trace into a function
104+
# TODO:
105+
# overload (instr::Instruction{F})() to specify
106+
# which function to trace into
107+
function trace_into end
108+
trace_into(x) = false
109+
74110
function run_and_record!(tape::Tape, f, args...)
75111
f = val(f) # f maybe a Boxed closure
76-
output = try
77-
box(f(map(val, args)...))
78-
catch e
79-
@warn e
80-
any_box(nothing)
112+
should_trace = trace_into(f)
113+
if !should_trace
114+
output = try
115+
box(f(map(val, args)...))
116+
catch e
117+
@warn e
118+
any_box(nothing)
119+
end
120+
ins = Instruction(f, args, output, tape)
121+
push!(tape, ins)
122+
return output
123+
else
124+
real_args = map(val, args)
125+
ir = IRTools.@code_ir f(real_args...)
126+
ir = intercept(ir; recorder=:run_and_record!)
127+
# 1. we should distinguish fixed args and varargs here
128+
arg_len = ir |> IRTools.arguments |> length
129+
arg_len -= 2 # 1 for f, 1 for vargs
130+
p_args = real_args[1:arg_len]
131+
v_args = arg_len < 1 ? real_args : real_args[arg_len+1:end]
132+
# detect if there is an vararg for f
133+
if length(v_args) == 1
134+
m = which(f, typeof(real_args))
135+
no_vararg = @static if VERSION >= v"1.7"
136+
(m.sig <: Tuple) && hasproperty(m.sig, :types) &&
137+
!isa(m.sig.types[end], Core.TypeofVararg)
138+
else
139+
(m.sig <: Tuple) && hasproperty(m.sig, :types) &&
140+
!(m.sig.types[end] <: Vararg{Any})
141+
end
142+
if no_vararg
143+
v_args = v_args[1]
144+
end
145+
end
146+
subtape = IRTools.evalir(ir, f, p_args..., v_args)
147+
# 2. we should recover the args after getting the tape
148+
# to keep the chain complete
149+
subtape[1].input = args
150+
ins = TapeInstruction(subtape, tape)
151+
output = subtape[end].output
152+
push!(tape, ins)
153+
return output
81154
end
82-
ins = Instruction(f, args, output, tape)
83-
push!(tape, ins)
84-
return output
85155
end
86156

87157
function dry_record!(tape::Tape, f, args...)
@@ -188,7 +258,7 @@ function (tf::TapedFunction)(args...)
188258
tape = IRTools.evalir(ir, tf.func, args...)
189259
tf.ir = ir
190260
tf.tape = tape
191-
tape.owner = tf
261+
setowner!(tape, tf)
192262
return result(tape)
193263
end
194264
# TODO: use cache

src/tapedtask.jl

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
struct TapedTaskException
22
exc
3+
backtrace
34
end
45

56
struct TapedTask
67
task::Task
78
tf::TapedFunction
8-
counter::Ref{Int}
99
produce_ch::Channel{Any}
1010
consume_ch::Channel{Int}
1111
produced_val::Vector{Any}
1212

1313
function TapedTask(
14-
t::Task, tf::TapedFunction, counter, pch::Channel{Any}, cch::Channel{Int})
15-
new(t, tf, counter, pch, cch, Any[])
14+
t::Task, tf::TapedFunction, pch::Channel{Any}, cch::Channel{Int})
15+
new(t, tf, pch, cch, Any[])
1616
end
1717
end
1818

1919
function TapedTask(tf::TapedFunction, args...)
2020
tf.owner != nothing && error("TapedFunction is owned to another task.")
2121
# dry_run(tf)
2222
isempty(tf.tape) && tf(args...)
23-
counter = Ref{Int}(1)
2423
produce_ch = Channel()
2524
consume_ch = Channel{Int}()
2625
task = @task try
27-
step_in(tf, counter, args)
26+
step_in(tf.tape, args)
2827
catch e
29-
put!(produce_ch, TapedTaskException(e))
30-
# @error "TapedTask Error: " exception=(e, catch_backtrace())
28+
bt = catch_backtrace()
29+
put!(produce_ch, TapedTaskException(e, bt))
30+
# @error "TapedTask Error: " exception=(e, bt)
3131
rethrow()
3232
finally
3333
@static if VERSION >= v"1.4"
@@ -40,7 +40,7 @@ function TapedTask(tf::TapedFunction, args...)
4040
close(produce_ch)
4141
close(consume_ch)
4242
end
43-
t = TapedTask(task, tf, counter, produce_ch, consume_ch)
43+
t = TapedTask(task, tf, produce_ch, consume_ch)
4444
task.storage === nothing && (task.storage = IdDict())
4545
task.storage[:tapedtask] = t
4646
tf.owner = t
@@ -53,25 +53,42 @@ TapedTask(f, args...) = TapedTask(TapedFunction(f, arity=length(args)), args...)
5353
TapedTask(t::TapedTask, args...) = TapedTask(func(t), args...)
5454
func(t::TapedTask) = t.tf.func
5555

56-
function step_in(tf::TapedFunction, counter::Ref{Int}, args)
57-
len = length(tf.tape)
58-
if(counter[] <= 1 && length(args) > 0)
56+
function step_in(t::Tape, args)
57+
len = length(t)
58+
if(t.counter <= 1 && length(args) > 0)
5959
input = map(box, args)
60-
tf.tape[1].input = input
60+
t[1].input = input
6161
end
62-
while counter[] <= len
63-
tf.tape[counter[]]()
62+
while t.counter <= len
63+
ins = t[t.counter]
64+
if isa(ins, TapeInstruction)
65+
step_in(ins.subtape, ())
66+
else
67+
ins()
68+
end
6469
# produce and wait after an instruction is done
65-
ttask = tf.owner
70+
ttask = t.owner.owner
6671
if length(ttask.produced_val) > 0
6772
val = pop!(ttask.produced_val)
6873
put!(ttask.produce_ch, val)
6974
take!(ttask.consume_ch) # wait for next consumer
7075
end
71-
counter[] += 1
76+
t.counter += 1
7277
end
7378
end
7479

80+
function increase_counter(t::Tape)
81+
t.counter > length(t) && return
82+
instr = t[t.counter]
83+
if isa(instr, TapeInstruction)
84+
increase_counter(instr.subtape)
85+
else
86+
# must be a produce instruction?
87+
t.counter += 1
88+
end
89+
end
90+
next_step(t::TapedTask) = increase_counter(t.tf.tape)
91+
7592
#=
7693
# ** Approach (A) to implement `produce`:
7794
# Make`produce` a standalone instturction. This approach does NOT
@@ -94,14 +111,15 @@ function (instr::Instruction{typeof(produce)})()
94111
end
95112
=#
96113

97-
98114
# ** Approach (B) to implement `produce`:
99115
# This way has its caveat:
100116
# `produce` may deeply hide in an instruction, but not be an instruction
101117
# itself, and when we copy a task, the newly copied task will resume from
102118
# the instruction after the one which contains this `produce` call. If the
103119
# call to `produce` is not the last expression in the instuction, that
104120
# instruction will not be whole executed in the copied task.
121+
# With the abilty to trace into nested function call, we can minimize the
122+
# limitation of this caveat.
105123
@inline function is_in_tapedtask()
106124
ct = current_task()
107125
ct.storage === nothing && return false
@@ -114,6 +132,8 @@ end
114132
function produce(val)
115133
is_in_tapedtask() || return nothing
116134
ttask = current_task().storage[:tapedtask]
135+
# put!(ttask.produce_ch, val)
136+
# take!(ttask.consume_ch) # wait for next consumer
117137
length(ttask.produced_val) > 1 &&
118138
error("There is a produced value which is not consumed.")
119139
push!(ttask.produced_val, val)
@@ -186,18 +206,26 @@ function copy_box(old_box::Box{T}, roster::Dict{UInt64, Any}) where T
186206
end
187207
copy_box(o, roster::Dict{UInt64, Any}) = o
188208

189-
function Base.copy(t::Tape)
209+
function Base.copy(x::Instruction, on_tape::Tape, roster::Dict{UInt64, Any})
210+
input = map(x.input) do ob
211+
copy_box(ob, roster)
212+
end
213+
output = copy_box(x.output, roster)
214+
Instruction(x.fun, input, output, on_tape)
215+
end
216+
217+
function Base.copy(x::TapeInstruction, on_tape::Tape, roster::Dict{UInt64, Any})
218+
subtape = copy(x.subtape, roster)
219+
TapeInstruction(subtape, on_tape)
220+
end
221+
222+
function Base.copy(t::Tape, roster::Dict{UInt64, Any})
190223
old_data = t.tape
191-
new_data = Vector{Instruction}()
192-
new_tape = Tape(new_data, t.owner)
224+
new_data = Vector{AbstractInstruction}()
225+
new_tape = Tape(new_data, t.counter, t.owner)
193226

194-
roster = Dict{UInt64, Any}()
195227
for x in old_data
196-
input = map(x.input) do ob
197-
copy_box(ob, roster)
198-
end
199-
output = copy_box(x.output, roster)
200-
new_ins = Instruction(x.fun, input, output, new_tape)
228+
new_ins = copy(x, new_tape, roster)
201229
push!(new_data, new_ins)
202230
end
203231

@@ -207,16 +235,18 @@ end
207235
function Base.copy(tf::TapedFunction)
208236
new_tf = TapedFunction(tf.func; arity=tf.arity)
209237
new_tf.ir = tf.ir
210-
new_tape = copy(tf.tape)
211-
new_tape.owner = new_tf
238+
roster = Dict{UInt64, Any}()
239+
new_tape = copy(tf.tape, roster)
240+
setowner!(new_tape, new_tf)
212241
new_tf.tape = new_tape
213242
return new_tf
214243
end
215244

216245
function Base.copy(t::TapedTask)
217-
# t.counter[] <= 1 && error("Can't copy a TapedTask which is not running.")
218246
tf = copy(t.tf)
219247
new_t = TapedTask(tf)
220-
new_t.counter[] = t.counter[] + 1
248+
new_t.task.storage = copy(t.task.storage)
249+
new_t.task.storage[:tapedtask] = new_t
250+
next_step(new_t)
221251
return new_t
222252
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ using Libtask
22
using Test
33

44
include("ctask.jl")
5+
include("special-instuctions.jl")
56
include("tarray.jl")
67
include("tref.jl")
78

test/special-instuctions.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@testset "Special Instructions" begin
2+
3+
@testset "TapeInstruction" begin
4+
i1(x) = i2(x)
5+
i2(x) = produce(x)
6+
7+
Libtask.trace_into(::typeof(i1)) = true
8+
Libtask.trace_into(::typeof(i2)) = true
9+
10+
function f()
11+
t = 0
12+
while t < 4
13+
i1(t)
14+
t = 1 + t
15+
end
16+
end
17+
18+
ctask = CTask(f)
19+
@test consume(ctask) == 0
20+
@test consume(ctask) == 1
21+
a = copy(ctask)
22+
@test consume(a) == 2
23+
@test consume(a) == 3
24+
@test consume(ctask) == 2
25+
@test consume(ctask) == 3
26+
end
27+
28+
end

0 commit comments

Comments
 (0)