about summary refs log tree commit diff stats
path: root/131table.subx
blob: 018e37ef370f8f660a3f16ff5270957f1b026ef5 (plain) (blame)
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
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Mu - 104new-segment.subx</title>
<meta name="Generator" content="Vim/8.1">
<meta name="plugin-version" content="vim8.1_v1">
<meta name="syntax" content="none">
<meta name="settings" content="number_lines,use_css,no_foldcolumn,expand_tabs,line_ids,prevent_copy=">
<meta name="colorscheme" content="minimal-light">
<style type="text/css">
<!--
pre { font-family: monospace; color: #000000; background-color: #c6c6c6; }
body { font-size:12pt; font-family: monospace; color: #000000; background-color: #c6c6c6; }
a { color:inherit; }
* { font-size:12pt; font-size: 1em; }
.subxComment { color: #005faf; }
.subxS2Comment { color: #8a8a8a; }
.subxFunction { color: #af5f00; text-decoration: underline; }
.LineNr { }
.subxS1Comment { color: #0000af; }
.SpecialChar { color: #d70000; }
.Normal { color: #000000; background-color: #c6c6c6; padding-bottom: 1px; }
.Constant { color: #008787; }
.subxMinorFunction { color: #875f5f; }
-->
</style>

<script type='text/javascript'>
<!--

/* function to open any folds containing a jumped-to line before jumping to it */
function JumpToLine()
{
  var lineNum;
  lineNum = window.location.hash;
  lineNum = lineNum.substr(1); /* strip off '#' */

  if (lineNum.indexOf('L') == -1) {
    lineNum = 'L'+lineNum;
  }
  var lineElem = document.getElementById(lineNum);
  /* Always jump to new location even if the line was hidden inside a fold, or
   * we corrected the raw number to a line ID.
   */
  if (lineElem) {
    lineElem.scrollIntoView(true);
  }
  return true;
}
if ('onhashchange' in window) {
  window.onhashchange = JumpToLine;
}

-->
</script>
</head>
<body onload='JumpToLine();'>
<a href='https://github.com/akkartik/mu/blob/master/104new-segment.subx'>https://github.com/akkartik/mu/blob/master/104new-segment.subx</a>
<pre id='vimCodeElement'>
<span id="L1" class="LineNr"> 1 </span><span class="subxComment"># Create a new segment (pool of memory for allocating chunks from) in the form</span>
<span id="L2" class="LineNr"> 2 </span><span class="subxComment"># of an *allocation descriptor* that can be passed to the memory allocator</span>
<span id="L3" class="LineNr"> 3 </span><span class="subxComment"># (defined in a later layer).</span>
<span id="L4" class="LineNr"> 4 </span><span class="subxComment">#</span>
<span id="L5" class="LineNr"> 5 </span><span class="subxComment"># Currently an allocation descriptor consists of just the bounds of the pool of</span>
<span id="L6" class="LineNr"> 6 </span><span class="subxComment"># available memory:</span>
<span id="L7" class="LineNr"> 7 </span><span class="subxComment">#</span>
<span id="L8" class="LineNr"> 8 </span><span class="subxComment">#   curr: address</span>
<span id="L9" class="LineNr"> 9 </span><span class="subxComment">#   end: address</span>
<span id="L10" class="LineNr">10 </span><span class="subxComment">#</span>
<span id="L11" class="LineNr">11 </span><span class="subxComment"># This isn't enough information to reclaim individual allocations. We can't</span>
<span id="L12" class="LineNr">12 </span><span class="subxComment"># support arbitrary reclamation yet.</span>
<span id="L13" class="LineNr">13 </span>
<span id="L14" class="LineNr">14 </span>== code
<span id="L15" class="LineNr">15 </span><span class="subxComment">#   instruction                     effective address                                                   register    displacement    immediate</span>
<span id="L16" class="LineNr">16 </span><span class="subxS1Comment"># . op          subop               mod             rm32          base        index         scale       r32</span>
<span id="L17" class="LineNr">17 </span><span class="subxS1Comment"># . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes</span>
<span id="L18" class="LineNr">18 </span>
<span id="L19" class="LineNr">19 </span><span class="SpecialChar">Entry</span>:   <span class="subxComment"># manual test</span>
<span id="L20" class="LineNr">20 </span>    <span class="subxComment"># var ad/ecx: allocation-descriptor</span>
<span id="L21" class="LineNr">21 </span>    68/push  0/imm32/limit
<span id="L22" class="LineNr">22 </span>    68/push  0/imm32/curr
<span id="L23" class="LineNr">23 </span>    89/copy                         3/mod/direct    1/rm32/ecx   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          4/r32/esp  <span class="Normal"> . </span>             <span class="Normal"> . </span>                <span class="subxComment"># copy esp to ecx</span>
<span id="L24" class="LineNr">24 </span>    <span class="subxComment"># new-segment(0x1000, ad)</span>
<span id="L25" class="LineNr">25 </span>    <span class="subxS2Comment"># . . push args</span>
<span id="L26" class="LineNr">26 </span>    51/push-ecx
<span id="L27" class="LineNr">27 </span>    68/push  0x1000/imm32
<span id="L28" class="LineNr">28 </span>    <span class="subxS2Comment"># . . call</span>
<span id="L29" class="LineNr">29 </span>    e8/call  <a href='104new-segment.subx.html#L40'>new-segment</a>/disp32
<span id="L30" class="LineNr">30 </span>    <span class="subxS2Comment"># . . discard args</span>
<span id="L31" class="LineNr">31 </span>    81          0/subop/add         3/mod/direct    4/rm32/esp   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>         <span class="Normal"> . </span>         <span class="Normal"> . </span>              8/imm32           <span class="subxComment"># add to esp</span>
<span id="L32" class="LineNr">32 </span>    <span class="subxComment"># var eax: (addr _) = ad-&gt;curr</span>
<span id="L33" class="LineNr">33 </span>    8b/copy                         0/mod/indirect  1/rm32/ecx   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax  <span class="Normal"> . </span>             <span class="Normal"> . </span>                <span class="subxComment"># copy *ecx to eax</span>
<span id="L34" class="LineNr">34 </span>    <span class="subxComment"># write to *eax to check that we have access to the newly-allocated segment</span>
<span id="L35" class="LineNr">35 </span>    c7          0/subop/copy        0/mod/direct    0/rm32/eax   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>         <span class="Normal"> . </span>         <span class="Normal"> . </span>              0x34/imm32        <span class="subxComment"># copy to *eax</span>
<span id="L36" class="LineNr">36 </span>    <span class="subxComment"># syscall(exit, eax)</span>
<span id="L37" class="LineNr">37 </span>    89/copy                         3/mod/direct    3/rm32/ebx   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax  <span class="Normal"> . </span>             <span class="Normal"> . </span>                <span class="subxComment"># copy eax to ebx</span>
<span id="L38" class="LineNr">38 </span>    e8/call  syscall_exit/disp32
<span id="L39" class="LineNr">39 </span>
<span id="L40" class="LineNr">40 </span><span class="subxFunction">new-segment</span>:  <span class="subxComment"># len: int, ad: (addr allocation-descriptor)</span>
<span id="L41" class="LineNr">41 </span>    <span class="subxS1Comment"># . prologue</span>
<span id="L42" class="LineNr">42 </span>    55/push-ebp
<span id="L43" class="LineNr">43 </span>    89/copy                         3/mod/direct    5/rm32/ebp   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          4/r32/esp  <span class="Normal"> . </span>             <span class="Normal"> . </span>                <span class="subxComment"># copy esp to ebp</span>
<span id="L44" class="LineNr">44 </span>    <span class="subxS1Comment"># . save registers</span>
<span id="L45" class="LineNr">45 </span>    50/push-eax
<span id="L46" class="LineNr">46 </span>    53/push-ebx
<span id="L47" class="LineNr">47 </span>    <span class="subxComment"># copy len to _mmap-new-segment-&gt;len</span>
<span id="L48" class="LineNr">48 </span>    8b/copy                         1/mod/*+disp8   5/rm32/ebp   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax   8/disp8        <span class="Normal"> . </span>                <span class="subxComment"># copy *(ebp+8) to eax</span>
<span id="L49" class="LineNr">49 </span>    89/copy                         0/mod/indirect  5/rm32/.disp32           <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax   $_mmap-new-segment:len/disp32     <span class="subxComment"># copy eax to *$_mmap-new-segment:len</span>
<span id="L50" class="LineNr">50 </span>    <span class="subxComment"># mmap(_mmap-new-segment)</span>
<span id="L51" class="LineNr">51 </span>    bb/copy-to-ebx  <a href='104new-segment.subx.html#L73'>_mmap-new-segment</a>/imm32
<span id="L52" class="LineNr">52 </span>    e8/call  syscall_mmap/disp32
<span id="L53" class="LineNr">53 </span>    <span class="subxComment"># copy {eax, eax+len} to *ad</span>
<span id="L54" class="LineNr">54 </span>    <span class="subxS1Comment"># . ebx = ad</span>
<span id="L55" class="LineNr">55 </span>    8b/copy                         1/mod/*+disp8   5/rm32/ebp   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          3/r32/ebx   0xc/disp8      <span class="Normal"> . </span>                <span class="subxComment"># copy *(ebp+12) to ebx</span>
<span id="L56" class="LineNr">56 </span>    <span class="subxS1Comment"># . ad-&gt;curr = eax</span>
<span id="L57" class="LineNr">57 </span>    89/copy                         0/mod/indirect  3/rm32/ebx   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax  <span class="Normal"> . </span>             <span class="Normal"> . </span>                <span class="subxComment"># copy eax to *ebx</span>
<span id="L58" class="LineNr">58 </span>    <span class="subxS1Comment"># . ad-&gt;end = eax+len</span>
<span id="L59" class="LineNr">59 </span>    03/add                          1/mod/*+disp8   5/rm32/ebp   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax   8/disp8        <span class="Normal"> . </span>                <span class="subxComment"># add *(ebp+8) to eax</span>
<span id="L60" class="LineNr">60 </span>    89/copy                         1/mod/*+disp8   3/rm32/ebx   <span class="Normal"> . </span>         <span class="Normal"> . </span>           <span class="Normal"> . </span>          0/r32/eax   4/disp8        <span class="Normal"> . </span>                <span class="subxComment"># copy eax to *(ebx+4)</span>
<span id="L61" class="LineNr">61 </span><span class="Constant">$new-segment:end</span>:
<span id="L62" class="LineNr">62 </span>    <span class="subxS1Comment"># . restore registers</span>
<span id="L63" class="LineNr">63 </span>    5b/pop-to-ebx
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# A table is a stream of (key, value) rows.
#
# Each row consists of an 8-byte key -- a (handle array byte) -- and a variable-size value.
#
# Accessing the table performs a linear scan for a key string, and always
# requires passing in the row size.
#
# Table primitives have the form <variant>(stream, <arg>, row-size, ...) -> address/eax
#
# The following table shows available options for <variant>:
#   if not found:           | arg=string              arg=slice
#   ------------------------+---------------------------------------------------
#   abort                   | get                     get-slice
#   insert key              | get-or-insert           get-or-insert-slice
#                           | get-or-insert-handle
#   stop                    | get-or-stop             get-slice-or-stop
#   return null             | maybe-get               maybe-get-slice
# Some variants may take extra args.

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

# if no row is found, abort
get:  # table: (addr stream {(handle array byte), T}), key: (addr array byte), row-size: int, abort-message-prefix: (addr array byte) -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if string-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   abort
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get:search-loop:
    # if (curr >= max) abort
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get:abort/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (string-equal?(key, c)) return curr+8
    # . eax = string-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  string-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get:end/disp8
$get:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $get:search-loop/disp8
$get:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get:abort:
    # . _write(2/stderr, abort-message-prefix)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x14/disp8      .                 # push *(ebp+20)
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . _write(2/stderr, error)
    # . . push args
    68/push  ": get: key not found: "/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . _write(2/stderr, key)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . _write(2/stderr, "\n")
    # . . push args
    68/push  Newline/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32
    # never gets here

test-get:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # - setup: create a table with a couple of keys
    # var table/ecx: (stream {(handle array byte), number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # insert(table, "data", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "data"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$test-get:check1:
    # eax = get(table, "code", 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get:check2:
    # eax = get(table, "data", 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    68/push  "data"/imm32
    51/push-ecx
    # . . call
    e8/call  get/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax - table->data, 20, msg)
    # . check-ints-equal(eax - table, 32, msg)
    # . . push args
    68/push  "F - test-get/1"/imm32
    68/push  0x20/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, abort
get-slice:  # table: (addr stream {(handle array byte), T}), key: (addr slice), row-size: int, abort-message-prefix: (addr array byte) -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if slice-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   abort
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get-slice:search-loop:
    # if (curr >= max) abort
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get-slice:abort/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (slice-equal?(key, c)) return curr+8
    # . eax = slice-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  slice-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get-slice:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get-slice:end/disp8
$get-slice:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $get-slice:search-loop/disp8
$get-slice:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get-slice:abort:
    # . _write(2/stderr, abort-message-prefix)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x14/disp8      .                 # push *(ebp+20)
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . _write(2/stderr, error)
    # . . push args
    68/push  ": get-slice: key not found: "/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-slice-buffered(Stderr, key)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    68/push  Stderr/imm32
    # . . call
    e8/call  write-slice-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . flush(Stderr)
    # . . push args
    68/push  Stderr/imm32
    # . . call
    e8/call  flush/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # . _write(2/stderr, "\n")
    # . . push args
    68/push  Newline/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32
    # never gets here

test-get-slice:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # - setup: create a table with a couple of keys
    # var table/ecx: (stream {string, number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # insert(table, "data", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "data"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$test-get-slice:check1:
    # (eax..edx) = "code"
    b8/copy-to-eax  "code"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
    05/add-to-eax  4/imm32
    # var slice/edx: slice = {eax, edx}
    52/push-edx
    50/push-eax
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # eax = get-slice(table, "code", 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  get-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax - table->data, 8, msg)  # first row's value slot returned
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-slice/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-slice:check2:
    # (eax..edx) = "data"
    b8/copy-to-eax  "data"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
    05/add-to-eax  4/imm32
    # var slice/edx: slice = {eax, edx}
    52/push-edx
    50/push-eax
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # eax = get-slice(table, "data" slice, 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  get-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax - table->data, 20, msg)
    # . check-ints-equal(eax - table, 32, msg)
    # . . push args
    68/push  "F - test-get-slice/1"/imm32
    68/push  0x20/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-slice:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, save 'key' to the next available row
# if there are no rows free, abort
# return the address of the value
get-or-insert:  # table: (addr stream {(handle array byte), T}), key: (addr array byte), row-size: int, ad: (addr allocation-descriptor) -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if string-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   if table->write >= table->size
    #     abort
    #   zero-out(max, row-size)
    #   copy-array(ad, key, max)
    #   table->write += row-size
    #   return max+8
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get-or-insert:search-loop:
    # if (curr >= max) break
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get-or-insert:not-found/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (string-equal?(key, c)) return curr+8
    # . eax = string-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  string-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get-or-insert:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get-or-insert:end/disp8
$get-or-insert:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $get-or-insert:search-loop/disp8
$get-or-insert:not-found:
    # if (table->write >= table->size) abort
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           1/r32/ecx   .               .                 # copy *esi to ecx
    3b/compare                      1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   8/disp8         .                 # compare ecx with *(esi+8)
    73/jump-if-addr>=  $get-or-insert:abort/disp8
    # zero-out(max, row-size)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    52/push-edx
    # . . call
    e8/call  zero-out/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # copy-array(ad, key, max)
    # . . push args
    52/push-edx
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x14/disp8      .                 # push *(ebp+20)
    # . . call
    e8/call  copy-array/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # table->write += row-size
    # . eax = row-size
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    # . table->write += eax
    01/add                          0/mod/indirect  6/rm32/esi    .           .             .           0/r32/eax   .               .                 # add eax to *esi
    # return max+8
    # . eax = max
    89/copy                         3/mod/direct    0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy edx to eax
    # . eax += 8
    05/add-to-eax  8/imm32
$get-or-insert:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get-or-insert:abort:
    # . _write(2/stderr, error)
    # . . push args
    68/push  "get-or-insert: table is full\n"/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32
    # never gets here

test-get-or-insert:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var table/ecx: (stream {(handle array byte), number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
$test-get-or-insert:first-call:
    # - start with an empty table, insert one key, verify that it was inserted
    # eax = get-or-insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # check-ints-equal(eax - table->data, 8, msg)  # first row's value slot returned
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-insert/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-get-or-insert/1"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var curr-addr/eax: (addr array byte) = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-get-or-insert/2"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert:second-call:
    # - insert the same key again, verify that it was reused
    # eax = get-or-insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-insert/3"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # no new row inserted
    # . check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-get-or-insert/4"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # curr-addr = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-get-or-insert/5"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert:third-call:
    # - insert a new key, verify that it was inserted
    # eax = get-or-insert(table, "data", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "data"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # table gets a new row
    # check-ints-equal(eax - table->data, 20, msg)  # second row's value slot returned
    # . check-ints-equal(eax - table, 32, msg)
    # . . push args
    68/push  "F - test-get-or-insert/6"/imm32
    68/push  0x20/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(table->write, 2 rows = 24, msg)
    # . . push args
    68/push  "F - test-get-or-insert/7"/imm32
    68/push  0x18/imm32/two-rows
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # curr-addr = lookup(table->data+12)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x1c/disp8      .                 # push *(ecx+28)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x18/disp8      .                 # push *(ecx+24)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "data", msg)
    # . . push args
    68/push  "F - test-get-or-insert/8"/imm32
    68/push  "data"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, save 'key' to the next available row
# if there are no rows free, abort
# return the address of the value
get-or-insert-handle:  # table: (addr stream {(handle array byte), T}), key: (handle array byte), row-size: int -> result/eax: (addr T)
    # pseudocode:
    #   var curr: (addr handle stream) = table->data
    #   var max: (addr byte) = &table->data[table->write]
    #   var k: (addr array byte) = lookup(key)
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if string-equal?(k, c)
    #       return curr+8
    #     curr += row-size
    #   if table->write >= table->size
    #     abort
    #   *max = key
    #   table->write += row-size
    #   return max+8
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    53/push-ebx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var k/ebx: (addr array byte) = lookup(key)
    # . eax = lookup(key)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . ebx = eax
    89/copy                         3/mod/direct    3/rm32/ebx    .           .             .           0/r32/eax   .               .                 # copy eax to ebx
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get-or-insert-handle:search-loop:
    # if (curr >= max) break
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get-or-insert-handle:not-found/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (string-equal?(k, c)) return curr+8
    # . eax = string-equal?(k, c)
    # . . push args
    50/push-eax
    53/push-ebx
    # . . call
    e8/call  string-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get-or-insert-handle:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get-or-insert-handle:end/disp8
$get-or-insert-handle:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x14/disp8      .                 # add *(ebp+20) to ecx
    # loop
    eb/jump  $get-or-insert-handle:search-loop/disp8
$get-or-insert-handle:not-found:
    # if (table->write >= table->size) abort
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           1/r32/ecx   .               .                 # copy *esi to ecx
    3b/compare                      1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   8/disp8         .                 # compare ecx with *(esi+8)
    73/jump-if-addr>=  $get-or-insert-handle:abort/disp8
    # table->write += row-size
    # . eax = row-size
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x14/disp8      .                 # copy *(ebp+20) to eax
    # . table->write += eax
    01/add                          0/mod/indirect  6/rm32/esi    .           .             .           0/r32/eax   .               .                 # add eax to *esi
    # *max = key
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0xc/disp8       .                 # copy *(ebp+12) to eax
    89/copy                         0/mod/indirect  2/rm32/edx    .           .             .           0/r32/eax   .               .                 # copy eax to *edx
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    89/copy                         1/mod/*+disp8   2/rm32/edx    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(edx+4)
    # return max+8
    # . eax = max
    89/copy                         3/mod/direct    0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy edx to eax
    # . eax += 8
    05/add-to-eax  8/imm32
$get-or-insert-handle:end:
    # . restore registers
    5e/pop-to-esi
    5b/pop-to-ebx
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get-or-insert-handle:abort:
    # . _write(2/stderr, error)
    # . . push args
    68/push  "get-or-insert-handle: table is full\n"/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32
    # never gets here

test-get-or-insert-handle:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var table/ecx: (stream {(handle array byte), number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var h/edx: (handle array byte)
    68/push  0/imm32
    68/push  0/imm32
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
$test-get-or-insert-handle:first-call:
    # - start with an empty table, insert one key, verify that it was inserted
    # copy-array(Heap, "code", h)
    # . . push args
    52/push-edx
    68/push  "code"/imm32
    68/push  Heap/imm32
    # . . call
    e8/call  copy-array/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # eax = get-or-insert-handle(table, h, 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    51/push-ecx
    # . . call
    e8/call  get-or-insert-handle/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # check-ints-equal(eax - table->data, 8, msg)  # first row's value slot returned
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/1"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var curr-addr/eax: (addr array byte) = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/2"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert-handle:second-call:
    # - insert the same key again, verify that it was reused
    # copy-array(Heap, "code", h)
    # . . push args
    52/push-edx
    68/push  "code"/imm32
    68/push  Heap/imm32
    # . . call
    e8/call  copy-array/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # eax = get-or-insert-handle(table, h, 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    51/push-ecx
    # . . call
    e8/call  get-or-insert-handle/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/3"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # no new row inserted
    # . check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/4"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # curr-addr = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/5"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert-handle:third-call:
    # - insert a new key, verify that it was inserted
    # copy-array(Heap, "data", h)
    # . . push args
    52/push-edx
    68/push  "data"/imm32
    68/push  Heap/imm32
    # . . call
    e8/call  copy-array/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # eax = get-or-insert-handle(table, h, 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    ff          6/subop/push        0/mod/indirect  2/rm32/edx    .           .             .           .           .               .                 # push *edx
    51/push-ecx
    # . . call
    e8/call  get-or-insert-handle/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # table gets a new row
    # check-ints-equal(eax - table->data, 20, msg)  # second row's value slot returned
    # . check-ints-equal(eax - table, 32, msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/6"/imm32
    68/push  0x20/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(table->write, 2 rows = 24, msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/7"/imm32
    68/push  0x18/imm32/two-rows
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # curr-addr = lookup(table->data+12)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x1c/disp8      .                 # push *(ecx+28)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x18/disp8      .                 # push *(ecx+24)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "data", msg)
    # . . push args
    68/push  "F - test-get-or-insert-handle/8"/imm32
    68/push  "data"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert-handle:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, save 'key' in the next available row
# if there are no rows free, abort
get-or-insert-slice:  # table: (addr stream {(handle array byte), T}), key: (addr slice), row-size: int, ad: (addr allocation-descriptor) -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if slice-equal?(key, *curr)
    #       return curr+8
    #     curr += row-size
    #   if table->write >= table->size
    #     abort
    #   zero-out(max, row-size)
    #   slice-to-string(ad, key, max)
    #   table->write += row-size
    #   return max+8
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get-or-insert-slice:search-loop:
    # if (curr >= max) break
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get-or-insert-slice:not-found/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (slice-equal?(key, c)) return curr+4
    # . eax = slice-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  slice-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get-or-insert-slice:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get-or-insert-slice:end/disp8
$get-or-insert-slice:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $get-or-insert-slice:search-loop/disp8
$get-or-insert-slice:not-found:
    # result/eax = 0
    31/xor                          3/mod/direct    0/rm32/eax    .           .             .           0/r32/eax   .               .                 # clear eax
    # if (table->write >= table->size) abort
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           1/r32/ecx   .               .                 # copy *esi to ecx
    3b/compare                      1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   8/disp8         .                 # compare ecx with *(esi+8)
    7d/jump-if->=  $get-or-insert-slice:abort/disp8
    # zero-out(max, row-size)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x10/disp8      .                 # push *(ebp+16)
    52/push-edx
    # . . call
    e8/call  zero-out/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # slice-to-string(ad, key, max)
    # . . push args
    52/push-edx
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x14/disp8      .                 # push *(ebp+20)
    # . . call
    e8/call  slice-to-string/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # table->write += row-size
    # . eax = row-size
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           0/r32/eax   0x10/disp8      .                 # copy *(ebp+16) to eax
    # . table->write += eax
    01/add                          0/mod/indirect  6/rm32/esi    .           .             .           0/r32/eax   .               .                 # add eax to *esi
    # return max+8
    # . eax = max
    89/copy                         3/mod/direct    0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy edx to eax
    # . eax += 8
    05/add-to-eax  8/imm32
$get-or-insert-slice:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get-or-insert-slice:abort:
    # . _write(2/stderr, error)
    # . . push args
    68/push  "get-or-insert-slice: table is full\n"/imm32
    68/push  2/imm32/stderr
    # . . call
    e8/call  _write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32
    # never gets here

test-get-or-insert-slice:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # var table/ecx: (stream {string, number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # (eax..edx) = "code"
    b8/copy-to-eax  "code"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
    05/add-to-eax  4/imm32
    # var slice/edx: slice = {eax, edx}
    52/push-edx
    50/push-eax
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
$test-get-or-insert-slice:first-call:
    # - start with an empty table, insert one key, verify that it was inserted
    # eax = get-or-insert-slice(table, "code" slice, 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  get-or-insert-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # check-ints-equal(eax - table->data, 8, msg)  # first row's value slot returned
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/1"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var curr-addr/eax: (addr array byte) = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/2"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert-slice:second-call:
    # - insert the same key again, verify that it was reused
    # eax = get-or-insert-slice(table, "code" slice, 12 bytes/row)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  get-or-insert-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/3"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # no new row inserted
    # . check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/4"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # curr-addr = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/5"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert-slice:third-call:
    # - insert a new key, verify that it was inserted
    # (eax..edx) = "data"
    b8/copy-to-eax  "data"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
    05/add-to-eax  4/imm32
    # var slice/edx: slice = {eax, edx}
    52/push-edx
    50/push-eax
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # eax = get-or-insert-slice(table, "data" slice, 12 bytes/row)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  get-or-insert-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # table gets a new row
    # check-ints-equal(eax - table->data, 20, msg)  # second row's value slot returned
    # . check-ints-equal(eax - table, 32, msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/6"/imm32
    68/push  0x20/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(table->write, 2 rows = 24, msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/7"/imm32
    68/push  0x18/imm32/two-rows
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # curr-addr = lookup(table->data+12)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x1c/disp8      .                 # push *(ecx+28)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x18/disp8      .                 # push *(ecx+24)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "data", msg)
    # . . push args
    68/push  "F - test-get-or-insert-slice/8"/imm32
    68/push  "data"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-insert-slice:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, stop(ed)
get-or-stop:  # table: (addr stream {(handle array byte), T}), key: (addr array byte), row-size: int,
              # abort-message-prefix: (addr array byte), err: (addr buffered-file), ed: (addr exit-descriptor)
              # -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if string-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   write-buffered(err, msg)
    #   stop(ed)
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get-or-stop:search-loop:
    # if (curr >= max) stop(ed)
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get-or-stop:stop/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (string-equal?(key, c)) return curr+8
    # . eax = string-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  string-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get-or-stop:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get-or-stop:end/disp8
$get-or-stop:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $get-or-stop:search-loop/disp8
$get-or-stop:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get-or-stop:stop:
    # . write-buffered(err, abort-message-prefix)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x14/disp8      .                 # push *(ebp+20)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-buffered(err, error)
    # . . push args
    68/push  ": get-or-stop: key not found: "/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-buffered(err, key)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-buffered(err, "\n")
    # . . push args
    68/push  Newline/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . stop(ed, 1)
    # . . push args
    68/push  1/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x1c/disp8      .                 # push *(ebp+28)
    # . . call
    e8/call  stop/disp32
    # never gets here
$get-or-stop:terminus:
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32

test-get-or-stop:
    # This test uses exit-descriptors. Use ebp for setting up local variables.
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # setup
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # . clear-stream($_test-error-buffered-file->buffer)
    # . . push args
    68/push  $_test-error-buffered-file->buffer/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # var table/ecx: (stream {string, number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var ed/edx: exit-descriptor
    68/push  0/imm32
    68/push  0/imm32
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # size 'ed' for the calls to 'get-or-stop'
    # . tailor-exit-descriptor(ed, 24)
    # . . push args
    68/push  0x18/imm32/nbytes-of-args-for-get-or-stop
    52/push-edx
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$test-get-or-stop:success:
    # eax = get-or-stop(table, "code", row-size=12, msg, _test-error-buffered-file, ed)
    # . . push args
    52/push-edx/ed
    68/push  _test-error-buffered-file/imm32
    68/push  "foo"/imm32/abort-prefix
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-stop/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # add to esp
$test-get-or-stop:success-assertion:
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-or-stop/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-stop:failure:
    # eax = get-or-stop(table, "data", row-size=12, msg, _test-error-buffered-file, ed)
    # . . push args
    52/push-edx/ed
    68/push  _test-error-buffered-file/imm32
    68/push  "foo"/imm32/abort-prefix
    68/push  0xc/imm32/row-size
    68/push  "data"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-stop/disp32
    # registers except esp may be clobbered at this point
    # restore register args, discard others
    59/pop-to-ecx
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    5a/pop-to-edx
$test-get-or-stop:failure-assertion:
    # check that get-or-stop tried to call stop(1)
    # . check-ints-equal(ed->value, 2, msg)
    # . . push args
    68/push  "F - test-get-or-stop/1"/imm32
    68/push  2/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-or-stop:end:
    # . epilogue
    # don't restore esp from ebp; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x2c/imm32        # add to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, stop(ed)
get-slice-or-stop:  # table: (addr stream {(handle array byte), _}), key: (addr slice), row-size: int,
                    # abort-message-prefix: (addr string), err: (addr buffered-file), ed: (addr exit-descriptor)
                    # -> result/eax: (addr _)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if slice-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   write-buffered(err, msg)
    #   stop(ed)
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$get-slice-or-stop:search-loop:
    # if (curr >= max) stop(ed)
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $get-slice-or-stop:stop/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (slice-equal?(key, c)) return curr+4
    # . eax = slice-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  slice-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $get-slice-or-stop:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $get-slice-or-stop:end/disp8
$get-slice-or-stop:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $get-slice-or-stop:search-loop/disp8
$get-slice-or-stop:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

$get-slice-or-stop:stop:
    # . write-buffered(err, abort-message-prefix)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x14/disp8      .                 # push *(ebp+20)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-buffered(err, error)
    # . . push args
    68/push  ": get-slice-or-stop: key not found: "/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-slice-buffered(err, key)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-slice-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . write-buffered(err, "\n")
    # . . push args
    68/push  Newline/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x18/disp8      .                 # push *(ebp+24)
    # . . call
    e8/call  write-buffered/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . stop(ed, 1)
    # . . push args
    68/push  1/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0x1c/disp8      .                 # push *(ebp+28)
    # . . call
    e8/call  stop/disp32
    # never gets here
$get-slice-or-stop:terminus:
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # syscall(exit, 1)
    bb/copy-to-ebx  1/imm32
    e8/call  syscall_exit/disp32

test-get-slice-or-stop:
    # This test uses exit-descriptors. Use ebp for setting up local variables.
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # setup
    # . clear-stream(_test-error-stream)
    # . . push args
    68/push  _test-error-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # . clear-stream($_test-error-buffered-file->buffer)
    # . . push args
    68/push  $_test-error-buffered-file->buffer/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # var table/ecx: (stream {string, number} 24)  # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # var ed/edx: exit-descriptor
    68/push  0/imm32
    68/push  0/imm32
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # var slice/ebx: slice = "code"
    # . (eax..ebx) = "code"
    b8/copy-to-eax  "code"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           3/r32/ebx   .               .                 # copy *eax to ebx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  3/index/ebx   .           3/r32/ebx   4/disp8         .                 # copy eax+ebx+4 to ebx
    05/add-to-eax  4/imm32
    # . ebx = {eax, ebx}
    53/push-ebx
    50/push-eax
    89/copy                         3/mod/direct    3/rm32/ebx    .           .             .           4/r32/esp   .               .                 # copy esp to ebx
    # size 'ed' for the calls to 'get-or-stop' (define no locals past this point)
    # . tailor-exit-descriptor(ed, 24)
    # . . push args
    68/push  0x18/imm32/nbytes-of-args-for-get-or-stop
    52/push-edx
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$test-get-slice-or-stop:success:
    # eax = get-slice-or-stop(table, slice, row-size=12, msg, _test-error-buffered-file, ed)
    # . . push args
    52/push-edx/ed
    68/push  _test-error-buffered-file/imm32
    68/push  "foo"/imm32/abort-prefix
    68/push  0xc/imm32/row-size
    53/push-ebx/slice
    51/push-ecx
    # . . call
    e8/call  get-slice-or-stop/disp32
    # registers except esp may be clobbered at this point
    # restore register args, discard others
    59/pop-to-ecx
    5b/pop-to-ebx
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    5a/pop-to-edx
$test-get-slice-or-stop:success-assertion:
    # check-ints-equal(eax - table->data, 8, msg)  # first row's value slot returned
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-get-slice-or-stop/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-slice-or-stop:failure:
    # slice = "segment2"
    # . *ebx = "segment2"->data
    b8/copy-to-eax  "segment2"/imm32
    05/add-to-eax  4/imm32
    89/copy                         0/mod/indirect  3/rm32/ebx    .           .             .           0/r32/eax   .               .                 # copy eax to *ebx
    # . *(ebx+4) = "segment2"->data + len("segment2")
    05/add-to-eax  8/imm32/strlen
    89/copy                         1/mod/*+disp8   3/rm32/ebx    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(ebx+4)
    # eax = get-slice-or-stop(table, slice, row-size=12, msg, _test-error-buffered-file, ed)
    # . . push args
    52/push-edx/ed
    68/push  _test-error-buffered-file/imm32
    68/push  "foo"/imm32/abort-prefix
    68/push  0xc/imm32/row-size
    53/push-ebx/slice
    51/push-ecx
    # . . call
    e8/call  get-slice-or-stop/disp32
    # registers except esp may be clobbered at this point
    # restore register args, discard others
    59/pop-to-ecx
    5b/pop-to-ebx
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    5a/pop-to-edx
$test-get-slice-or-stop:failure-assertion:
    # check that get-or-stop tried to call stop(1)
    # . check-ints-equal(ed->value, 2, msg)
    # . . push args
    68/push  "F - test-get-or-stop/1"/imm32
    68/push  2/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   2/rm32/edx    .           .             .           .           4/disp8         .                 # push *(edx+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-get-slice-or-stop:end:
    # . epilogue
    # don't restore esp from ebp; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x34/imm32        # add to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, return null (0)
maybe-get:  # table: (addr stream {(handle array byte), T}), key: (addr array byte), row-size: int -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if string-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   return 0
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$maybe-get:search-loop:
    # if (curr >= max) return null
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $maybe-get:null/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (string-equal?(key, c)) return curr+4
    # . eax = string-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  string-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $maybe-get:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $maybe-get:end/disp8
$maybe-get:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $maybe-get:search-loop/disp8
$maybe-get:null:
    b8/copy-to-eax  0/imm32
$maybe-get:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-maybe-get:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # - setup: create a table with one row
    # var table/ecx: (stream {string, number} 24)   # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # eax = get-or-insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$test-maybe-get:success:
    # - check for the same key, verify that it was reused
    # eax = maybe-get(table, "code", 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  maybe-get/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-maybe-get/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # no new row inserted
    # . check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-maybe-get/1"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var curr-addr/eax: (addr array byte) = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-maybe-get/2"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-maybe-get:failure:
    # - search for a new key
    # eax = maybe-get(table, "data", 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    68/push  "data"/imm32
    51/push-ecx
    # . . call
    e8/call  maybe-get/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax, 0, msg)
    # . . push args
    68/push  "F - test-maybe-get/3"/imm32
    68/push  0/imm32
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-maybe-get:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# if no row is found, return null (0)
maybe-get-slice:  # table: (addr stream {(handle array byte), T}), key: (addr slice), row-size: int -> result/eax: (addr T)
    # pseudocode:
    #   curr = table->data
    #   max = &table->data[table->write]
    #   while curr < max
    #     var c: (addr array byte) = lookup(*curr)
    #     if slice-equal?(key, c)
    #       return curr+8
    #     curr += row-size
    #   return 0
    #
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = table
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # var curr/ecx: (addr handle array byte) = table->data
    8d/copy-address                 1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   0xc/disp8       .                 # copy esi+12 to ecx
    # var max/edx: (addr byte) = &table->data[table->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           2/r32/edx   .               .                 # copy *esi to edx
    8d/copy-address                 0/mod/indirect  4/rm32/sib    1/base/ecx  2/index/edx   .           2/r32/edx   .               .                 # copy ecx+edx to edx
$maybe-get-slice:search-loop:
    # if (curr >= max) return null
    39/compare                      3/mod/direct    1/rm32/ecx    .           .             .           2/r32/edx   .               .                 # compare ecx with edx
    73/jump-if-addr>=  $maybe-get-slice:null/disp8
    # var c/eax: (addr array byte) = lookup(*curr)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           4/disp8         .                 # push *(ecx+4)
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # if (slice-equal?(key, c)) return curr+4
    # . eax = slice-equal?(key, c)
    # . . push args
    50/push-eax
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           0xc/disp8       .                 # push *(ebp+12)
    # . . call
    e8/call  slice-equal?/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # . if (eax != false) return eax = curr+8
    3d/compare-eax-and  0/imm32/false
    74/jump-if-=  $maybe-get-slice:mismatch/disp8
    8d/copy-address                 1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   8/disp8         .                 # copy ecx+8 to eax
    eb/jump  $maybe-get-slice:end/disp8
$maybe-get-slice:mismatch:
    # curr += row-size
    03/add                          1/mod/*+disp8   5/rm32/ebp    .           .             .           1/r32/ecx   0x10/disp8      .                 # add *(ebp+16) to ecx
    # loop
    eb/jump  $maybe-get-slice:search-loop/disp8
$maybe-get-slice:null:
    b8/copy-to-eax  0/imm32
$maybe-get-slice:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-maybe-get-slice:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # - setup: create a table with one row
    # var table/ecx: (stream {string, number} 24)   # 2 rows * 12 bytes/row
    81          5/subop/subtract    3/mod/direct    4/rm32/esp    .           .             .           .           .               0x18/imm32        # subtract from esp
    68/push  0x18/imm32/size
    68/push  0/imm32/read
    68/push  0/imm32/write
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # insert(table, "code", 12 bytes/row, Heap)
    # . . push args
    68/push  Heap/imm32
    68/push  0xc/imm32/row-size
    68/push  "code"/imm32
    51/push-ecx
    # . . call
    e8/call  get-or-insert/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
$test-maybe-get-slice:success:
    # - check for the same key, verify that it was reused
    # (eax..edx) = "code"
    b8/copy-to-eax  "code"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
    05/add-to-eax  4/imm32
    # var slice/edx: slice = {eax, edx}
    52/push-edx
    50/push-eax
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # eax = maybe-get-slice(table, "code" slice, 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  maybe-get-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax - table->data, 8, msg)
    # . check-ints-equal(eax - table, 20, msg)
    # . . push args
    68/push  "F - test-maybe-get-slice/0"/imm32
    68/push  0x14/imm32
    29/subtract                     3/mod/direct    0/rm32/eax    .           .             .           1/r32/ecx   .               .                 # subtract ecx from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # no new row inserted
    # . check-ints-equal(table->write, row-size = 12, msg)
    # . . push args
    68/push  "F - test-maybe-get-slice/1"/imm32
    68/push  0xc/imm32/row-size
    ff          6/subop/push        0/mod/indirect  1/rm32/ecx    .           .             .           .           .               .                 # push *ecx
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # var curr-addr/eax: (addr array byte) = lookup(table->data)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0x10/disp8      .                 # push *(ecx+16)
    ff          6/subop/push        1/mod/*+disp8   1/rm32/ecx    .           .             .           .           0xc/disp8       .                 # push *(ecx+12)
    # . . call
    e8/call  lookup/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-strings-equal(curr-addr, "code", msg)
    # . . push args
    68/push  "F - test-maybe-get-slice/2"/imm32
    68/push  "code"/imm32
    50/push-eax
    # . . call
    e8/call  check-strings-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-maybe-get-slice:failure:
    # - search for a new key
    # (eax..edx) = "data"
    b8/copy-to-eax  "data"/imm32
    8b/copy                         0/mod/indirect  0/rm32/eax    .           .             .           2/r32/edx   .               .                 # copy *eax to edx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    0/base/eax  2/index/edx   .           2/r32/edx   4/disp8         .                 # copy eax+edx+4 to edx
    05/add-to-eax  4/imm32
    # var slice/edx: slice = {eax, edx}
    52/push-edx
    50/push-eax
    89/copy                         3/mod/direct    2/rm32/edx    .           .             .           4/r32/esp   .               .                 # copy esp to edx
    # eax = maybe-get-slice(table, "data" slice, 12 bytes/row)
    # . . push args
    68/push  0xc/imm32/row-size
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  maybe-get-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(eax, 0, msg)
    # . . push args
    68/push  "F - test-maybe-get-slice/3"/imm32
    68/push  0/imm32
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
$test-maybe-get-slice:end:
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

# . . vim:nowrap:textwidth=0