mindcode
mindcode copied to clipboard
Feature Request - Decompiler
This one is very optional, it's kind of an idea for a side project.
How about you create a tool where you paste in MLOG (compiled) and get (very bad) mindcode out. It's gonna have gotos in it and very simple statements that do one operation at a time.
You might have a (reasonable) objection to this on the grounds that you're "borrowing" other player's logic.
This idea has crossed my mind too.
Right off the bat, some mlog code can't really be expressed in Mindcode at all. Anything that depends on direct assignment to @counter would be problematic. It might be possible to detect simple function calls, but if there's an expression, it might be mightily problematic to just figure out all the possible targets of such a jump.
Secondly, the gotos. I don't want to add a goto to the language, because it would completely break some optimizations. Most importantly the DFO has specific handling for if/case statements and especially loops. Generic gotos would require a complete rewrite of the entire DFO to operate on the control flow graph of the code the way modern compilers do (which could actually be a benefit), but that's a lot of work for very little gain.
On the other hand, if the mlog code was well structured, it might be possible to decompile it into structured code, with while / do-while loops and conditional statements. And I guess that even spaghetti-code should be representable in a structured way, at the price of duplicating code shared by multiple code paths. Just this analysis alone would be an interesting problem :) However, I think it would be much more useful to continue developing the compiler/optimizer at this point.
You might have a (reasonable) objection to this on the grounds that you're "borrowing" other player's logic.
That's a thorny issue, especially as most of the mlog code around doesn't come with a license, but we'd be just providing a tool. And a good decompiler would provide a way for new users to quickly convert their existing code into something they can work on. That would be a huge benefit.
Okay, so perhaps an escape hatch here...
Just translate the code you can trivially translate, then put the untranslated MLOG code in comment blocks around it. It could even be linear flow (translator does not handle jumps at all) or simple if statements only.
Leave handling the other jumps as an exercise for the programmer.
You might consider adding a vanilla MLOG construct to mindcode where an author can put raw MLOG in (asm in C for instance) and put the untranslated code in that.
Edit: Of course jumps will likely be very broken with the asm approach since the number of MLOG statements output by the compiler will change (which of course if why you should use mindcode not MLOG).
Just translate the code you can trivially translate, then put the untranslated MLOG code in comment blocks around it. It could even be linear flow (translator does not handle jumps at all) or simple if statements only.
Yes, that could be a viable approach. Jumps would be commented out and the target of the jump would be marked with a label, probably commented out again. Later on, structures could be detected in the compiled code (maybe ifs and while/do-while loops).
Assignments to the @counter in any way or form would be totally off, of course.
You might consider adding a vanilla MLOG construct to mindcode where an author can put raw MLOG in (asm in C for instance) and put the untranslated code in that.
I've been thinking about ams/mlog blocks too. The compiler would have to understand them at least a bit (so that it can properly replace variable identifiers in the mlog code with the name generated by the compiler, e.g. in case of local variables). As far as jumps go, jumps outside of the asm/mlog blocks wouldn't be allowed, so it's not a solution for decompiling jumps.
What about something like this:
def foo(a, b)
mlog(a, b, c)
op mul x a a;
op mul y b b;
op add c x y;
end;
return c;
end;
I'd require semicolons between instructions so that I wouldn't have to parse newlines. end instruction wouldn't be allowed: firstly it clashes with the end keyword, secondly it would be a jump outside of the mlog block, which is disallowed.
The list of variables after the mlog keyword says that a, b and c are variables that have to be replaced by the actual variable name - as these are local variables, they'll become something like __fn0_a, __fn0_b and __fn0_c. A constant could be specified here too, in which case it would be replaced by the mlog literal value.
The other problem is using other variables in the embedded mlog. They could easily collide with variables outside the mlog block - in the example, x and y could be main variables. It would be up to the programmer to make sure the variables in the mlog block do not collide with anything.
Jumps would have to be made using labels, and the compiler would have to understand them. As far as I know, Mindustry can parse mlog code which does use jump labels; however, I need to be able to obtain instruction index for jump tables, and there doesn't really appear to be a way to do that with labels. So I'd have to handle the labels and convert them to instruction indexes, meaning I'd need to parse at least the jump instructions.
So, mlog blocks wouldn't be useful for decompiling. Would you find them useful anyway?
Would you find them useful anyway?
No...
Personally, I'd prefer to stick to 100% mindcode. Experience teaches me that it is never worth opening the asm escape hatch. Better to make readable code.
Your syntax is fine.
The other problem is using other variables in the embedded mlog. They could easily collide with variables outside the mlog block
If you were to add this (questionable) feature, you could rename all variables used by the author so they have a __mlog_ prefix or a __<username>_mlog (part suffix). That way different mlog blocks can communicate but they don't interfere with local vars.
Once a var is declared in your mlog block (eg a in mlog(a,b,c)) it no longer has the variable name mangling. Though that might raise a warning if a DIFFERENT mlog block uses the same variable but doesn't make it global.
I have a habit sometimes of suggesting inpractical ideas. Feel free to just drop this one.
If you were to add this (questionable) feature, you could rename all variables used by the author so they have a
__mlog_prefix or a__<username>_mlog(part suffix). That way different mlog blocks can communicate but they don't interfere with local vars.
This is only possible for instructions that are known to Mindcode, as it needs to figure out which of the arguments to the instructions are keywords and which are variables and only to mangle the variables. But if only known instructions are supported, the mlog block wouldn't be usable to encode unknown instructions (they might come from a mod, or from a pre-release Mindustry version, or perhaps when I don't get to update Mindcode to match with new Mindustry Logic quickly enough). And being able to embed unknown instructions was the motivation for me.
On the other hand, I might add a way to encode unknown instructions to Mindcode - either by having a special user defined function which would define how to encode the instruction, or by allowing to add new records to the Mindcode instruction metadata (which are used to create functions from). Hm, that addresses my goal directly and is probably more user friendly.
Let's pretend Mindcode doesn't know the ucontrol getBlock and I want to define it as a new mlog instruction:
inline def getBlock(in x, in y, out type, out floor)
declare building out;
mlog("ucontrol", "getBlock", x, y, type, building, floor);
return building;
end;
So, the mlog function would do all the magic: it would take all the arguments and produce a new, generic instruction by concatenating them. String constants, known keywords and the @ literals would be encoded as given, and variables would be encoded using the mlog names of them. I should probably allow escaping the double quotes in the strings here.
The instruction generated by the mlog function will know which arguments are input and which are output (this is crucial for the DFO to work correctly - actually the mlog block might break the DFO anyway) based on the declaration of the enclosing function's parameters. When used outside a function, or when a variable isn't among function parameters, it will be possible to mark the variable as in or out using the declare keyword.
This is more versatile and seems both easier and more intuitive than trying to extend Mindcode instruction metadata.
There's a bunch of new instruction in the unreleased Mindustry version. I could create a file that would define functions for new instructions without adding them to the baseline Mindcode version. That would be pretty neat :)
An mlog function that uses the out or ref keywords we discussed in another issue sounds ideal here.
mlog("ucontrol", "getBlock", x, y, type, out building, floor);
Mods and other updates are a good motivation to support it.
Wrapping these in user-functions would be a great idea from a style perspective
I have prototype decompiler ready. Here's a hand-written mlog code (this was before I learned I could use symbolic labels in mlog):
set MIN_BATCH 10
set MAX_BATCH 1000
set n 0
set lastCell 511
getlink memory n
sensor type memory @type
jump 13 equal type @memory-cell
jump 14 equal type @memory-bank
op add n n 1
jump 4 lessThan n @links
print "Item Counter\nNo memory attached."
printflush message1
end
set lastCell 63
read total memory 0
op max MIN_BATCH MIN_BATCH 10
op idiv MIN_BATCH MIN_BATCH 10
op mul MIN_BATCH MIN_BATCH 10
op max MAX_BATCH MAX_BATCH MIN_BATCH
op idiv MAX_BATCH MAX_BATCH 10
op mul MAX_BATCH MAX_BATCH 10
set BATCH_STEP 10
set batch MIN_BATCH
set startTime @time
set startTick @tick
set rate 0
set ratePerMin 0
op add limit total batch
set n 0
getlink block n
op add n n 1
sensor type block @type
jump 52 notEqual type @plastanium-conveyor
read prev memory n
sensor curr block @totalItems
write curr memory n
jump 52 greaterThanEq curr prev
op add total total 10
write total memory 0
jump 52 lessThan total limit
set tick @tick
op add limit total batch
op sub duration tick startTick
op div rate batch duration
write rate memory lastCell
op mul ratePerMin rate 3600
op floor ratePerMin ratePerMin 0
set startTick tick
set startTime @time
jump 52 greaterThan duration 120
op add batch batch BATCH_STEP
op min batch batch MAX_BATCH
jump 29 lessThan n @links
op sub elapsed @time startTime
jump 60 lessThan elapsed 10000
set rate 0
op idiv batch batch 90
op mul batch batch 10
op max batch batch MIN_BATCH
jump 44 always 0 0
print "Item Counter \nTotal items: "
print total
print "\nRate (items/min): "
print ratePerMin
print "\nBatch size: "
print batch
print "\nBatch time (ms): "
print elapsed
printflush message1
sensor button switch1 @enabled
jump 28 equal button 0
control enabled switch1 0 0 0 0
set n 0
write 0 memory lastCell
write 0 memory n
op add n n 1
jump 74 lessThanEq n @links
The decompiled output:
MIN_BATCH = 10;
MAX_BATCH = 1000;
n = 0;
lastCell = 511;
label2:
memory = getlink(n);
type = memory.sensor(@type);
if type == @memory-cell then goto label0;
if type == @memory-bank then goto label1;
n = n + 1;
if n < @links then goto label2;
print("Item Counter\nNo memory attached.");
message1.printflush();
end();
label0:
lastCell = 63;
label1:
total = memory[0];
MIN_BATCH = max(MIN_BATCH, 10);
MIN_BATCH = MIN_BATCH \ 10;
MIN_BATCH = MIN_BATCH * 10;
MAX_BATCH = max(MAX_BATCH, MIN_BATCH);
MAX_BATCH = MAX_BATCH \ 10;
MAX_BATCH = MAX_BATCH * 10;
BATCH_STEP = 10;
batch = MIN_BATCH;
startTime = @time;
startTick = @tick;
rate = 0;
ratePerMin = 0;
limit = total + batch;
label7:
n = 0;
label4:
block = getlink(n);
n = n + 1;
type = block.sensor(@type);
if type != @plastanium-conveyor then goto label3;
prev = memory[n];
curr = block.sensor(@totalItems);
memory[n] = curr;
if curr >= prev then goto label3;
total = total + 10;
memory[0] = total;
if total < limit then goto label3;
tick = @tick;
limit = total + batch;
duration = tick - startTick;
rate = batch / duration;
label6:
memory[lastCell] = rate;
ratePerMin = rate * 3600;
ratePerMin = floor(ratePerMin);
startTick = tick;
startTime = @time;
if duration > 120 then goto label3;
batch = batch + BATCH_STEP;
batch = min(batch, MAX_BATCH);
label3:
if n < @links then goto label4;
elapsed = @time - startTime;
if elapsed < 10000 then goto label5;
rate = 0;
batch = batch \ 90;
batch = batch * 10;
batch = max(batch, MIN_BATCH);
goto label6;
label5:
print("Item Counter \nTotal items: ");
print(total);
print("\nRate (items/min): ");
print(ratePerMin);
print("\nBatch size: ");
print(batch);
print("\nBatch time (ms): ");
print(elapsed);
message1.printflush();
button = switch1.sensor(@enabled);
if button == 0 then goto label7;
switch1.enabled = 0;
n = 0;
memory[lastCell] = 0;
label8:
memory[n] = 0;
n = n + 1;
if n <= @links then goto label8;
Here's how I've rewritten it (long time back - not using the decompiler):
MIN_BATCH = 10;
MAX_BATCH = 1000;
MEMORY = null;
while MEMORY == null do
print("Item Counter");
n = @links;
while n > 0 do
n = n - 1;
block = getlink(n);
case block.type
when @memory-cell then
MEMORY = block;
lastCell = 63;
when @memory-bank then
MEMORY = block;
lastCell = 511;
end;
end;
if MEMORY == null then
print("\nNo memory attached.");
end;
printflush(message1);
end;
total = MEMORY[0];
BATCH_DURATION = 120;
MIN_BATCH = (max(MIN_BATCH, 10) \ 10) * 10;
MAX_BATCH = (max(MAX_BATCH, MIN_BATCH) \ 10) * 10;
BATCH_STEP = 10;
batch = MIN_BATCH;
startTime = @time;
startTick = @tick;
rate = 0;
ratePerMin = 0;
limit = total + batch;
switch1.enabled = 0;
while switch1.enabled == 0 do
start = @time;
n = 0;
while n < @links do
block = getlink(n);
n += 1;
if block.type == @plastanium-conveyor then
prev = MEMORY[n];
curr = block.totalItems;
MEMORY[n] = curr;
if curr < prev then
total += 10;
MEMORY[0] = total;
if total > limit then
tick = @tick;
limit = total + batch;
duration = tick - startTick;
rate = batch / duration;
MEMORY[lastCell] = rate;
ratePerMin = floor(rate * 3600);
startTick = tick;
startTime = @time;
if duration <= BATCH_DURATION then
batch = min(batch + BATCH_STEP, MAX_BATCH);
end;
end;
end;
end;
end;
elapsed = @time - startTime;
if elapsed >= 10000 then
rate = 0;
batch = max((batch \ 90) * 10, MIN_BATCH);
MEMORY[lastCell] = 0;
ratePerMin = 0;
startTick = tick;
startTime = @time;
end;
print("Item Counter \nTotal items: ", total);
print("\nRate (items/min): ", ratePerMin);
print("\nBatch size: ", batch);
print("\nBatch time (ms): ", elapsed);
print("\nLoop time (ms): ", @time - start);
printflush(message1);
end;
switch1.enabled = 0;
MEMORY[lastCell] = 0;
n = 0;
while n < @links do
MEMORY[n] = 0;
n += 1;
end;
Running the decompiler on compiled Mindcode doesn't produce very nice output, but decompiling compiled Mindcode doesn't make much sense. A code written in a high-level language (e.g. mlogjs) will always be better rewritten into Mindcode straight away instead of decompiling it from mlog.
That looks like nice output!
decompiling compiled Mindcode doesn't make much sense.
It is however a likely use-case if you copied a blueprint during a multiplayer game and you want to read / modify someone else's compiled script.
But even the functionality you've provided would be enough to re-write someone else's script piece by piece (after disassembly)
It is however a likely use-case if you copied a blueprint during a multiplayer game and you want to read / modify someone else's compiled script.
That would be nice if true :) I'd expect there would be much more hand-written mlog floating around than Mindcode (or another high-level language) generated mlog.
There are potentially (at least) two ways to improve the output:
- Collapsing expressions
- Resolving high-level structures (loops, conditions, function calls).
The first one seems to be easier of the two, and maybe even more useful. It should turn this
batch = batch \ 90;
batch = batch * 10;
batch = max(batch, MIN_BATCH);
into
batch = max((batch \ 90) * 10, MIN_BATCH);
I would want to propagate these expressions even into the if statements. This should improve decompiling Mindcode (or any other high level compiler) generated code quite a bit.
Here's a decompiled Mindcode output (the Building example from the web application) for comparison. Collapsing expressions would remove a lot of the __tmpXX variables.
goto label0;
print("Configurable options:");
label0:
MEMORY = cell1;
UNIT = @poly;
SW_X = 0;
SW_Y = 0;
NE_X = @mapw;
NE_Y = @maph;
DOWNGRADE = false;
WIDTH = 10;
RADIUS = 10;
goto label1;
print("Don't modify anything below this line.");
label1:
TOTAL = 0;
__tmp0 = MEMORY[0];
if __tmp0 == false then goto label2;
__fn0_n = MEMORY[1];
__tmp65 = max(__fn0_n, SW_X);
__fn0retval = min(__tmp65, NE_X);
x = __fn0retval;
__fn0_n = MEMORY[2];
__tmp65 = max(__fn0_n, SW_Y);
__fn0retval = min(__tmp65, NE_Y);
y = __fn0retval;
dx = -1;
__tmp6 = MEMORY[3];
if __tmp6 != 1 then goto label3;
dx = 1;
label3:
dy = -1 * WIDTH;
__tmp9 = MEMORY[4];
if __tmp9 <= 0 then goto label4;
dy = WIDTH;
label4:
TOTAL = MEMORY[5];
goto label5;
label2:
MEMORY[0] = true;
MEMORY[1] = SW_X;
x = SW_X;
MEMORY[2] = SW_Y;
y = SW_Y;
dx = 1;
dy = WIDTH;
label5:
__tmp14 = @conveyor;
if DOWNGRADE == false then goto label6;
__tmp14 = @titanium-conveyor;
label6:
oldType = __tmp14;
__tmp15 = @titanium-conveyor;
if DOWNGRADE == false then goto label7;
__tmp15 = @conveyor;
label7:
newType = __tmp15;
__tmp32 = WIDTH \ 2;
__tmp16 = switch1.sensor(@enabled);
if __tmp16 != false then goto label8;
label22:
__tmp18 = @unit.sensor(@dead);
__tmp19 = __tmp18 === 0;
__tmp20 = __tmp19 == false;
__tmp21 = @unit.sensor(@controller);
__tmp22 = __tmp21 != @this;
__tmp23 = __tmp20 | __tmp22;
if __tmp23 == false then goto label9;
label13:
ubind(UNIT);
if @unit != null then goto label10;
print("No unit of type ");
print(UNIT);
print(" found.");
goto label11;
label10:
__tmp28 = @unit.sensor(@controlled);
if __tmp28 == 0 then goto label12;
print("Looking for a free ");
print(UNIT);
print("...");
goto label11;
label12:
__tmp31 = rand(1000);
flag(__tmp31);
goto label9;
label11:
message1.printflush();
goto label13;
label9:
__fn0_n = y + __tmp32;
__tmp65 = max(__fn0_n, SW_Y);
__fn0retval = min(__tmp65, NE_Y);
ypos = __fn0retval;
__tmp35 = within(x, __fn0retval, RADIUS);
if __tmp35 != false then goto label14;
label15:
move(x, __fn0retval);
__tmp35 = within(x, __fn0retval, RADIUS);
if __tmp35 == false then goto label15;
label14:
__tmp37 = y + WIDTH;
__tmp38 = __tmp37 - 1;
__tmp40 = min(NE_Y, __tmp38);
yrep = y;
if y > __tmp40 then goto label16;
label19:
__fn2_building = getBlock(x, yrep, __fn2_type?, 0?);
if __fn2_type != __tmp14 then goto label17;
__tmp46 = __fn2_building.sensor(@rotation);
build(x, yrep, __tmp15, __tmp46, 0);
label18:
0 = getBlock(x, yrep, __fn2_type?, 0?);
if __fn2_type != __tmp15 then goto label18;
TOTAL = TOTAL + 1;
label17:
yrep = yrep + 1;
if yrep <= __tmp40 then goto label19;
label16:
MEMORY[5] = TOTAL;
print("Position: ");
print(x);
print(", ");
print(__fn0retval);
print("\nUpgrades: ");
print(TOTAL);
print("\n");
message1.printflush();
x = x + dx;
__tmp52 = x > NE_X;
__tmp53 = x < SW_X;
__tmp54 = __tmp52 | __tmp53;
if __tmp54 == false then goto label20;
dx = -1 * dx;
y = y + dy;
__tmp65 = max(x, SW_X);
__fn0retval = min(__tmp65, NE_X);
x = __fn0retval;
MEMORY[3] = dx;
__tmp59 = y > NE_Y;
__tmp60 = y < SW_Y;
__tmp61 = __tmp59 | __tmp60;
if __tmp61 == false then goto label21;
dy = -1 * dy;
__tmp65 = max(y, SW_Y);
__fn0retval = min(__tmp65, NE_Y);
y = __fn0retval;
MEMORY[4] = dy;
label21:
MEMORY[2] = y;
label20:
MEMORY[1] = x;
__tmp16 = switch1.sensor(@enabled);
if __tmp16 == false then goto label22;
label8:
end();
print("Compiled by Mindcode - github.com/cardillan/mindcode");
Previous example with collapsed expressions:
goto label0;
print("Configurable options:");
// label0:
MEMORY = cell1;
UNIT = @poly;
SW_X = 0;
SW_Y = 0;
NE_X = @mapw;
NE_Y = @maph;
DOWNGRADE = false;
WIDTH = 10;
RADIUS = 10;
goto label1;
print("Don't modify anything below this line.");
// label1:
TOTAL = 0;
__tmp0 = MEMORY[0];
if !__tmp0 then goto label2;
__fn0_n = MEMORY[1];
__fn0retval = min(max(__fn0_n, SW_X), NE_X);
x = __fn0retval;
__fn0_n = MEMORY[2];
__fn0retval = min(max(__fn0_n, SW_Y), NE_Y);
y = __fn0retval;
dx = -1;
__tmp6 = MEMORY[3];
if __tmp6 != 1 then goto label3;
dx = 1;
// label3:
dy = -1 * WIDTH;
__tmp9 = MEMORY[4];
if __tmp9 <= 0 then goto label4;
dy = WIDTH;
// label4:
TOTAL = MEMORY[5];
goto label5;
// label2:
MEMORY[0] = true;
MEMORY[1] = SW_X;
x = SW_X;
MEMORY[2] = SW_Y;
y = SW_Y;
dx = 1;
dy = WIDTH;
// label5:
__tmp14 = @conveyor;
if !DOWNGRADE then goto label6;
__tmp14 = @titanium-conveyor;
// label6:
oldType = __tmp14;
__tmp15 = @titanium-conveyor;
if !DOWNGRADE then goto label7;
__tmp15 = @conveyor;
// label7:
newType = __tmp15;
__tmp32 = WIDTH \ 2;
__tmp16 = switch1.sensor(@enabled);
if __tmp16 then goto label8;
// label22:
__tmp18 = @unit.sensor(@dead);
__tmp21 = @unit.sensor(@controller);
if !(((__tmp18 === 0) == false) | (__tmp21 != @this)) then goto label9;
// label13:
ubind(UNIT);
if @unit != null then goto label10;
print("No unit of type ");
print(UNIT);
print(" found.");
goto label11;
// label10:
__tmp28 = @unit.sensor(@controlled);
if __tmp28 == 0 then goto label12;
print("Looking for a free ");
print(UNIT);
print("...");
goto label11;
// label12:
flag(rand(1000));
goto label9;
// label11:
message1.printflush();
goto label13;
// label9:
__fn0retval = min(max((y + __tmp32), SW_Y), NE_Y);
ypos = __fn0retval;
__tmp35 = within(x, __fn0retval, RADIUS);
if __tmp35 then goto label14;
// label15:
move(x, __fn0retval);
__tmp35 = within(x, __fn0retval, RADIUS);
if !__tmp35 then goto label15;
// label14:
yrep = y;
if y > min(NE_Y, ((y + WIDTH) - 1)) then goto label16;
// label19:
__fn2_building = getBlock(x, yrep, __fn2_type?, 0?);
if __fn2_type != __tmp14 then goto label17;
__tmp46 = __fn2_building.sensor(@rotation);
build(x, yrep, __tmp15, __tmp46, 0);
// label18:
0 = getBlock(x, yrep, __fn2_type?, 0?);
if __fn2_type != __tmp15 then goto label18;
TOTAL = TOTAL + 1;
// label17:
yrep = yrep + 1;
if yrep <= __tmp40 then goto label19;
// label16:
MEMORY[5] = TOTAL;
print("Position: ");
print(x);
print(", ");
print(__fn0retval);
print("\nUpgrades: ");
print(TOTAL);
print("\n");
message1.printflush();
x = x + dx;
if !((x > NE_X) | (x < SW_X)) then goto label20;
dx = -1 * dx;
y = y + dy;
__fn0retval = min(max(x, SW_X), NE_X);
x = __fn0retval;
MEMORY[3] = dx;
if !((y > NE_Y) | (y < SW_Y)) then goto label21;
dy = -1 * dy;
__fn0retval = min(max(y, SW_Y), NE_Y);
y = __fn0retval;
MEMORY[4] = dy;
// label21:
MEMORY[2] = y;
// label20:
MEMORY[1] = x;
__tmp16 = switch1.sensor(@enabled);
if !__tmp16 then goto label22;
// label8:
end();
print("Compiled by Mindcode - github.com/cardillan/mindcode");
Notice, for example
// label14:
yrep = y;
if y > min(NE_Y, ((y + WIDTH) - 1)) then goto label16;
versus earlier
// label14:
__tmp37 = y + WIDTH;
__tmp38 = __tmp37 - 1;
__tmp40 = min(NE_Y, __tmp38);
yrep = y;
if y > __tmp40 then goto label16;
There's still plenty of room for improvement, but it will do for now.
The (very basic) decompiler is part of 2.4.0 release.
The mlog function is on my todo list.
Closing.