about summary refs log tree commit diff stats
path: root/031merge.cc
blob: c56579a5a942ce08defac6bf9a28dad5a520398c (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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//: Construct types out of their constituent fields.

:(scenario merge)
container foo [
  x:num
  y:num
]
def main [
  1:foo <- merge 3, 4
]
+mem: storing 3 in location 1
+mem: storing 4 in location 2

:(before "End Primitive Recipe Declarations")
MERGE,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "merge", MERGE);
:(before "End Primitive Recipe Checks")
case MERGE: {
  // type-checking in a separate transform below
  break;
}
:(before "End Primitive Recipe Implementations")
case MERGE: {
  products.resize(1);
  for (int i = 0;  i < SIZE(ingredients);  ++i)
    for (int j = 0;  j < SIZE(ingredients.at(i));  ++j)
      products.at(0).push_back(ingredients.at(i).at(j));
  break;
}

//: type-check 'merge' to avoid interpreting numbers as addresses

:(scenario merge_check)
def main [
  1:point <- merge 3, 4
]
$error: 0

:(scenario merge_check_missing_element)
% Hide_errors = true;
def main [
  1:point <- merge 3
]
+error: main: too few ingredients in '1:point <- merge 3'

:(scenario merge_check_extra_element)
% Hide_errors = true;
def main [
  1:point <- merge 3, 4, 5
]
+error: main: too many ingredients in '1:point <- merge 3, 4, 5'

//: We want to avoid causing memory corruption, but other than that we want to
//: be flexible in how we construct containers of containers. It should be
//: equally easy to define a container out of primitives or intermediate
//: container fields.

:(scenario merge_check_recursive_containers)
def main [
  1:point <- merge 3, 4
  1:point-number <- merge 1:point, 5
]
$error: 0

:(scenario merge_check_recursive_containers_2)
% Hide_errors = true;
def main [
  1:point <- merge 3, 4
  2:point-number <- merge 1:point
]
+error: main: too few ingredients in '2:point-number <- merge 1:point'

:(scenario merge_check_recursive_containers_3)
def main [
  1:point-number <- merge 3, 4, 5
]
$error: 0

:(scenario merge_check_recursive_containers_4)
% Hide_errors = true;
def main [
  1:point-number <- merge 3, 4
]
+error: main: too few ingredients in '1:point-number <- merge 3, 4'

:(scenario merge_check_reflexive)
% Hide_errors = true;
def main [
  1:point <- merge 3, 4
  2:point <- merge 1:point
]
$error: 0

//: Since a container can be merged in several ways, we need to be able to
//: backtrack through different possibilities. Later we'll allow creating
//: exclusive containers which contain just one of rather than all of their
//: elements. That will also require backtracking capabilities. Here's the
//: state we need to maintain for backtracking:

:(before "End Types")
struct merge_check_point {
  reagent container;
  int container_element_index;
  merge_check_point(const reagent& c, int i) :container(c), container_element_index(i) {}
};

struct merge_check_state {
  stack<merge_check_point> data;
};

:(before "End Checks")
Transform.push_back(check_merge_calls);  // idempotent
:(code)
void check_merge_calls(const recipe_ordinal r) {
  const recipe& caller = get(Recipe, r);
  trace(9991, "transform") << "--- type-check merge instructions in recipe " << caller.name << end();
  for (int i = 0;  i < SIZE(caller.steps);  ++i) {
    const instruction& inst = caller.steps.at(i);
    if (inst.name != "merge") continue;
    if (SIZE(inst.products) != 1) {
      raise << maybe(caller.name) << "'merge' should yield a single product in '" << to_original_string(inst) << "'\n" << end();
      continue;
    }
    reagent/*copy*/ product = inst.products.at(0);
    // Update product While Type-checking Merge
    const type_tree* product_base_type = product.type->atom ? product.type : product.type->left;
    assert(product_base_type->atom);
    if (product_base_type->value == 0 || !contains_key(Type, product_base_type->value)) {
      raise << maybe(caller.name) << "'merge' should yield a container in '" << to_original_string(inst) << "'\n" << end();
      continue;
    }
    const type_info& info = get(Type, product_base_type->value);
    if (info.kind != CONTAINER && info.kind != EXCLUSIVE_CONTAINER) {
      raise << maybe(caller.name) << "'merge' should yield a container in '" << to_original_string(inst) << "'\n" << end();
      continue;
    }
    check_merge_call(inst.ingredients, product, caller, inst);
  }
}

void check_merge_call(const vector<reagent>& ingredients, const reagent& product, const recipe& caller, const instruction& inst) {
  int ingredient_index = 0;
  merge_check_state state;
  state.data.push(merge_check_point(product, 0));
  while (true) {
    assert(!state.data.empty());
    trace("transform") << ingredient_index << " vs " << SIZE(ingredients) << end();
    if (ingredient_index >= SIZE(ingredients)) {
      raise << maybe(caller.name) << "too few ingredients in '" << to_original_string(inst) << "'\n" << end();
      return;
    }
    reagent& container = state.data.top().container;
    if (!container.type) return;  // error handled elsewhere
    const type_tree* top_root_type = container.type->atom ? container.type : container.type->left;
    assert(top_root_type->atom);
    type_info& container_info = get(Type, top_root_type->value);
    switch (container_info.kind) {
      case CONTAINER: {
        // degenerate case: merge with the same type always succeeds
        if (state.data.top().container_element_index == 0 && types_coercible(container, inst.ingredients.at(ingredient_index)))
          return;
        const reagent& expected_ingredient = element_type(container.type, state.data.top().container_element_index);
        trace("transform") << "checking container " << to_string(container) << " || " << to_string(expected_ingredient) << " vs ingredient " << ingredient_index << end();
        // if the current element is the ingredient we expect, move on to the next element/ingredient
        if (types_coercible(expected_ingredient, ingredients.at(ingredient_index))) {
          ++ingredient_index;
          ++state.data.top().container_element_index;
          while (state.data.top().container_element_index >= SIZE(get(Type, get_base_type(state.data.top().container.type)->value).elements)) {
            state.data.pop();
            if (state.data.empty()) {
              if (ingredient_index < SIZE(ingredients))
                raise << maybe(caller.name) << "too many ingredients in '" << to_original_string(inst) << "'\n" << end();
              return;
            }
            ++state.data.top().container_element_index;
          }
        }
        // if not, maybe it's a field of the current element
        else {
          // no change to ingredient_index
          state.data.push(merge_check_point(expected_ingredient, 0));
        }
        break;
      }
      // End check_merge_call Special-cases
      default: {
        if (!types_coercible(container, ingredients.at(ingredient_index))) {
          raise << maybe(caller.name) << "incorrect type of ingredient " << ingredient_index << " in '" << to_original_string(inst) << "'\n" << end();
          raise << "  (expected '" << debug_string(container) << "')\n" << end();
          raise << "  (got '" << debug_string(ingredients.at(ingredient_index)) << "')\n" << end();
          return;
        }
        ++ingredient_index;
        // ++state.data.top().container_element_index;  // unnecessary, but wouldn't do any harm
        do {
          state.data.pop();
          if (state.data.empty()) {
            if (ingredient_index < SIZE(ingredients))
              raise << maybe(caller.name) << "too many ingredients in '" << to_original_string(inst) << "'\n" << end();
            return;
          }
          ++state.data.top().container_element_index;
        } while (state.data.top().container_element_index >= SIZE(get(Type, get_base_type(state.data.top().container.type)->value).elements));
      }
    }
  }
  // never gets here
  assert(false);
}

//: replaced in a later layer
//: todo: find some clean way to take this call completely out of this layer
const type_tree* get_base_type(const type_tree* t) {
  return t;
}

:(scenario merge_check_product)
% Hide_errors = true;
def main [
  1:num <- merge 3
]
+error: main: 'merge' should yield a container in '1:num <- merge 3'

:(before "End Includes")
#include <stack>
using std::stack;
/span> # todo: create a notion of iterator and iterable so we can read/write whole # aggregates (arrays, lists, ..) of _elems at once. scenario channel-initialization [ run [ local-scope source:&:source:num <- new-channel 3/capacity chan:&:channel:num <- get *source, chan:offset 10:num/raw <- get *chan, first-full:offset 11:num/raw <- get *chan, first-free:offset ] memory-should-contain [ 10 <- 0 # first-full 11 <- 0 # first-free ] ] scenario channel-write-increments-free [ local-scope _, sink:&:sink:num <- new-channel 3/capacity run [ sink <- write sink, 34 chan:&:channel:num <- get *sink, chan:offset 10:num/raw <- get *chan, first-full:offset 11:num/raw <- get *chan, first-free:offset ] memory-should-contain [ 10 <- 0 # first-full 11 <- 1 # first-free ] ] scenario channel-read-increments-full [ local-scope source:&:source:num, sink:&:sink:num <- new-channel 3/capacity sink <- write sink, 34 run [ _, _, source <- read source chan:&:channel:num <- get *source, chan:offset 10:num/raw <- get *chan, first-full:offset 11:num/raw <- get *chan, first-free:offset ] memory-should-contain [ 10 <- 1 # first-full 11 <- 1 # first-free ] ] scenario channel-wrap [ local-scope # channel with just 1 slot source:&:source:num, sink:&:sink:num <- new-channel 1/capacity chan:&:channel:num <- get *source, chan:offset # write and read a value sink <- write sink, 34 _, _, source <- read source run [ # first-free will now be 1 10:num/raw <- get *chan, first-free:offset 11:num/raw <- get *chan, first-free:offset # write second value, verify that first-free wraps sink <- write sink, 34 20:num/raw <- get *chan, first-free:offset # read second value, verify that first-full wraps _, _, source <- read source 30:num/raw <- get *chan, first-full:offset ] memory-should-contain [ 10 <- 1 # first-free after first write 11 <- 1 # first-full after first read 20 <- 0 # first-free after second write, wrapped 30 <- 0 # first-full after second read, wrapped ] ] scenario channel-new-empty-not-full [ run [ local-scope source:&:source:num <- new-channel 3/capacity chan:&:channel:num <- get *source, chan:offset 10:bool/raw <- channel-empty? chan 11:bool/raw <- channel-full? chan ] memory-should-contain [ 10 <- 1 # empty? 11 <- 0 # full? ] ] scenario channel-write-not-empty [ local-scope source:&:source:num, sink:&:sink:num <- new-channel 3/capacity chan:&:channel:num <- get *source, chan:offset run [ sink <- write sink, 34 10:bool/raw <- channel-empty? chan 11:bool/raw <- channel-full? chan ] memory-should-contain [ 10 <- 0 # empty? 11 <- 0 # full? ] ] scenario channel-write-full [ local-scope source:&:source:num, sink:&:sink:num <- new-channel 1/capacity chan:&:channel:num <- get *source, chan:offset run [ sink <- write sink, 34 10:bool/raw <- channel-empty? chan 11:bool/raw <- channel-full? chan ] memory-should-contain [ 10 <- 0 # empty? 11 <- 1 # full? ] ] scenario channel-read-not-full [ local-scope source:&:source:num, sink:&:sink:num <- new-channel 1/capacity chan:&:channel:num <- get *source, chan:offset sink <- write sink, 34 run [ _, _, source <- read source 10:bool/raw <- channel-empty? chan 11:bool/raw <- channel-full? chan ] memory-should-contain [ 10 <- 1 # empty? 11 <- 0 # full? ] ] scenario channel-clear [ local-scope # create a channel with a few items source:&:source:num, sink:&:sink:num <- new-channel 3/capacity chan:&:channel:num <- get *sink, chan:offset write sink, 30 write sink, 31 write sink, 32 run [ clear source 10:bool/raw <- channel-empty? chan ] memory-should-contain [ 10 <- 1 # after the call to 'clear', the channel should be empty ] ] def clear in:&:source:_elem -> in:&:source:_elem [ local-scope load-inputs chan:&:channel:_elem <- get *in, chan:offset { empty?:bool <- channel-empty? chan break-if empty? _, _, in <- read in loop } ] ## cancelling channels # every channel comes with a boolean signifying if it's been closed # initially this boolean is false container channel:_elem [ closed?:bool ] # a channel can be closed from either the source or the sink # both routines can modify the 'closed?' bit, but they can only ever set it, so this is a benign race def close x:&:source:_elem -> x:&:source:_elem [ local-scope load-inputs chan:&:channel:_elem <- get *x, chan:offset *chan <- put *chan, closed?:offset, true ] def close x:&:sink:_elem -> x:&:sink:_elem [ local-scope load-inputs chan:&:channel:_elem <- get *x, chan:offset *chan <- put *chan, closed?:offset, true ] # once a channel is closed from one side, no further operations are expected from that side # if a channel is closed for reading, # no further writes will be let through # if a channel is closed for writing, # future reads continue until the channel empties, # then the channel is also closed for reading after <channel-write-initial> [ closed?:bool <- get *chan, closed?:offset return-if closed? ] after <channel-read-empty> [ closed?:bool <- get *chan, closed?:offset { break-unless closed? empty-result:&:_elem <- new _elem:type current-routine-is-unblocked return *empty-result, true } ] ## helpers # An empty channel has first-free and first-full both at the same value. def channel-empty? chan:&:channel:_elem -> result:bool [ local-scope load-inputs # return chan.first-full == chan.first-free full:num <- get *chan, first-full:offset free:num <- get *chan, first-free:offset result <- equal full, free ] # A full channel has first-free just before first-full, wasting one slot. # (Other alternatives: https://www.snellman.net/blog/archive/2016-12-13-ring-buffers) def channel-full? chan:&:channel:_elem -> result:bool [ local-scope load-inputs # tmp = chan.first-free + 1 tmp:num <- get *chan, first-free:offset tmp <- add tmp, 1 { # if tmp == chan.capacity, tmp = 0 len:num <- capacity chan at-end?:bool <- greater-or-equal tmp, len break-unless at-end? tmp <- copy 0 } # return chan.first-full == tmp full:num <- get *chan, first-full:offset result <- equal full, tmp ] def capacity chan:&:channel:_elem -> result:num [ local-scope load-inputs q:&:@:_elem <- get *chan, data:offset result <- length *q ] ## helpers for channels of characters in particular def buffer-lines in:&:source:char, buffered-out:&:sink:char -> buffered-out:&:sink:char, in:&:source:char [ local-scope load-inputs # repeat forever eof?:bool <- copy false { line:&:buffer:char <- new-buffer 30 # read characters from 'in' until newline, copy into line { +next-character c:char, eof?:bool, in <- read in break-if eof? # drop a character on backspace { # special-case: if it's a backspace backspace?:bool <- equal c, 8 break-unless backspace? # drop previous character { buffer-length:num <- get *line, length:offset buffer-empty?:bool <- equal buffer-length, 0 break-if buffer-empty? buffer-length <- subtract buffer-length, 1 *line <- put *line, length:offset, buffer-length } # and don't append this one loop +next-character } # append anything else line <- append line, c line-done?:bool <- equal c, 10/newline break-if line-done? loop } # copy line into 'buffered-out' i:num <- copy 0 line-contents:text <- get *line, data:offset max:num <- get *line, length:offset { done?:bool <- greater-or-equal i, max break-if done? c:char <- index *line-contents, i buffered-out <- write buffered-out, c i <- add i, 1 loop } { break-unless eof? buffered-out <- close buffered-out return } loop } ] scenario buffer-lines-blocks-until-newline [ run [ local-scope source:&:source:char, sink:&:sink:char <- new-channel 10/capacity _, buffered-stdin:&:sink:char/buffered-stdin <- new-channel 10/capacity buffered-chan:&:channel:char <- get *buffered-stdin, chan:offset empty?:bool <- channel-empty? buffered-chan assert empty?, [ F buffer-lines-blocks-until-newline: channel should be empty after init] # buffer stdin into buffered-stdin, try to read from buffered-stdin buffer-routine:num <- start-running buffer-lines, source, buffered-stdin wait-for-routine-to-block buffer-routine empty? <- channel-empty? buffered-chan assert empty?:bool, [ F buffer-lines-blocks-until-newline: channel should be empty after buffer-lines bring-up] # write 'a' sink <- write sink, 97/a restart buffer-routine wait-for-routine-to-block buffer-routine empty? <- channel-empty? buffered-chan assert empty?:bool, [ F buffer-lines-blocks-until-newline: channel should be empty after writing 'a'] # write 'b' sink <- write sink, 98/b restart buffer-routine wait-for-routine-to-block buffer-routine empty? <- channel-empty? buffered-chan assert empty?:bool, [ F buffer-lines-blocks-until-newline: channel should be empty after writing 'b'] # write newline sink <- write sink, 10/newline restart buffer-routine wait-for-routine-to-block buffer-routine empty? <- channel-empty? buffered-chan data-emitted?:bool <- not empty? assert data-emitted?, [ F buffer-lines-blocks-until-newline: channel should contain data after writing newline] trace 1, [test], [reached end] ] trace-should-contain [ test: reached end ] ] def drain source:&:source:char -> result:text, source:&:source:char [ local-scope load-inputs buf:&:buffer:char <- new-buffer 30 { c:char, done?:bool <- read source break-if done? buf <- append buf, c loop } result <- buffer-to-array buf ]