about summary refs log blame commit diff stats
path: root/archive/2.transect/compiler9
blob: 26becf48a8b12fcf4099f29c438d7305e3b9297c (plain) (tree)





























































































































































































































































                                                                                                                                             
=== Goal

A memory-safe language with a simple translator to x86 that can be feasibly
written without itself needing a translator.

Memory-safe: it should be impossible to:
  a) create a pointer out of arbitrary data, or
  b) to access heap memory after it's been freed.

Simple: do all the work in a 2-pass translator:
  Pass 1: check each instruction's types in isolation.
  Pass 2: emit code for each instruction in isolation.

=== Overview of the language

A program consists of a series of type, function and global variable declarations.
(Also constants and tests, but let's focus on these.)

Type declarations basically follow Hindley-Milner with product and (tagged) sum
types. Types are written in s-expression form. There's a `ref` type that's a
type-safe fat pointer, with an alloc id that gets incremented after each
allocation. Memory allocation and reclamation is manual. Dereferencing a ref
after its underlying memory is reclaimed (pointer alloc id no longer matches
payload alloc id) is guaranteed to immediately kill the program (like a
segfault).

  # product type
  type foo [
    x : int
    y : (ref int)
    z : bar
  ]

  # sum type
  choice bar [
    x : int
    y : point
  ]

Functions have a header and a series of instructions in the body:

  fn f a : int -> b : int [
    ...
  ]

Instructions have the following format:

  io1, io2, ... <- operation i1, i2, ...

i1, i2 operands on the right hand side are immutable. io1, io2 are in-out
operands. They're written to, and may also be read.

User-defined functions will be called with the same syntax. They'll translate
to a sequence of push instructions (one per operand, both in and in-out), a
call instruction, and a sequence of pop instructions, either to a black hole
(in operands) or a location (in-out operands). This follows the standard Unix
calling convention. Each operand needs to be something push/pop can accept.

Primitive operations depend on the underlying processor. We'd like each primitive
operation supported by the language to map to a single instruction in the ISA.
Sometimes we have to violate that (see below), but we definitely won't be
writing to any temporary locations behind the scenes. The language affords
control over registers, and tracking unused registers gets complex, and
besides we may have no unused registers at a specific point. Instructions only
modify their operands.

In most ISAs, instructions operate on at most a word of data at a time. They
also tend to not have more than 2-3 operands, and not modify more than 2
locations in memory.

Since the number of reads from memory is limited, we break up complex high-level
operations using a special type called `address`. Addresses are strictly
short-term entities. They can't be stored in a compound type, and they can't
be passed into or returned from a user-defined function. They also can't be
used after a function call (because it could free the underlying memory) or
label (because it gets complex to check control flow, and we want to translate
each instruction simply and in isolation).

=== Compilation to 32-bit x86

Values can be stored:
  in code (literals)
  in registers
  on the stack
  on the global segment

Variables on the stack are stored at *(ESP+n)
Global variables are stored at *disp32, where disp32 is statically known

Address variables have to be in a register.
  - You need them in a register to do a lookup, and
  - Saving them to even the stack increases the complexity of checks needed on
    function calls or labels.

Compilation proceeds by pattern matching over an instruction along with
knowledge about the types of its operands, as well as where they're stored
(register/stack/global). We now enumerate mappings for various categories of
instructions, based on the type and location of their operands.

Where types of operands aren't mentioned below, all operands of an instruction
should have the same (word-length) type.

Lots of special cases because of limitations of the x86 ISA. Beware.

A. x : int <- add y

  Requires y to be scalar. Result will always be an int. No pointer arithmetic.

  reg <- add literal    => 81 0/subop 3/mod                                                                                           ...(0)
  reg <- add reg        => 01 3/mod                                                                                                   ...(1)
  reg <- add stack      => 03 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                        ...(2)
  reg <- add global     => 03 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(3)
  stack <- add literal  => 81 0/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32                          ...(4)
  stack <- add reg      => 01 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                        ...(5)
  stack <- add stack    => disallowed
  stack <- add global   => disallowed
  global <- add literal => 81 0/subop 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32                                         ...(6)
  global <- add reg     => 01 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                       ...(7)
  global <- add stack   => disallowed
  global <- add global  => disallowed

Similarly for sub, and, or, xor and even copy. Replace the opcodes above with corresponding ones from this table:

                            add             sub           and           or            xor         copy/mov
  reg <- op literal         81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
  reg <- op reg             01 or 03        29 or 2b      21 or 23      09 or 0b      31 or 33    89 or 8b
  reg <- op stack           03              2b            23            0b            33          8b
  reg <- op global          03              2b            23            0b            33          8b
  stack <- op literal       81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
  stack <- op reg           01              29            21            09            31          89
  global <- op literal      81 0/subop      81 5/subop    81 4/subop    81 1/subop    81 6/subop  c7
  global <- op reg          01              29            21            09            31          89

B. x/reg : int <- mul y

  Requires both y to be scalar.
  x must be in a register. Multiplies can't write to memory.

  reg <- mul literal    => 69                                                                                                         ...(8)
  reg <- mul reg        => 0f af 3/mod                                                                                                ...(9)
  reg <- mul stack      => 0f af 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32                                     ...(10)
  reg <- mul global     => 0f af 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                    ...(11)

C. x/EAX/quotient : int, y/EDX/remainder : int <- idiv z     # divide EAX by z; store the result in EAX and EDX

  Requires source x and z to both be scalar.
  x must be in EAX and y must be in EDX. Divides can't write anywhere else.

  First clear EDX (we don't support ints larger than 32 bits):
  31/xor 3/mod 2/rm32/EDX 2/r32/EDX

  then:
  EAX, EDX <- idiv literal  => disallowed
  EAX, EDX <- idiv reg      => f7 7/subop 3/mod                                                                                       ...(12)
  EAX, EDX <- idiv stack    => f7 7/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8                                    ...(13)
  EAX, EDX <- idiv global   => f7 7/subop 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                           ...(14)

D. x : int <- not

  Requires x to be an int.

  reg <- not                => f7 3/mod                                                                                               ...(15)
  stack <- not              => f7 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8                                            ...(16)
  global <- not             => f7 0/mod 5/rm32/include-disp32 global/disp32 reg/r32                                                   ...(17)

E. x : (address t) <- get o : T, %f

  (Assumes T.f has type t.)

  o can't be on a register since it's a non-primitive (likely larger than a word)
  f is a literal
  x must be in a register (by definition for an address)

  below '*' works on either address or ref types

  For raw stack values we want to read *(ESP+n)
  For raw global values we want to read *disp32
  For address stack values we want to read *(ESP+n)+
    *(ESP+n) contains an address
    so we want to compute *(ESP+n) + literal

  reg1 <- get reg2, literal       => 8d/lea 1/mod reg2/rm32 literal/disp8 reg1/r32                                                    ...(18)
  reg <- get stack, literal       => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(19)
    (simplifying assumption: stack frames can't be larger than 256 bytes)
  reg <- get global, literal      => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(20)

F. x : (offset T) <- index i : int, %size(T)

  reg1 <- index reg2, literal       => 69/mul 3/mod reg2/rm32 literal/imm32 -> reg1/r32
                                    or 68/mul 3/mod reg2/rm32 literal/imm8 -> reg1/r32                                                ...(21)
  reg1 <- index stack, literal      => 69/mul 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32 -> reg1/r32      ...(22)
  reg1 <- index global, literal     => 69/mul 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32 -> reg1/r32                     ...(23)

  optimization: avoid multiply if literal is a power of 2
    use SIB byte if literal is 2, 4 or 8
    or left shift

G. x : (address T) <- advance o : (array T), idx : (offset T)

  reg <- advance a/reg, idx/reg   => 8d/lea 0/mod 4/rm32/SIB a/base idx/index 0/scale reg/r32                                         ...(24)
  reg <- advance stack, literal   => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32                  ...(25)
  reg <- advance stack, reg2      => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP reg2/index 0/scale n/disp8 reg/r32                            ...(26)
  reg <- advance global, literal  => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32                                ...(27)

  also instructions for runtime bounds checking

=== Example

Putting it all together: code generation for `a[i].y = 4` where a is an array
of 2-d points with x, y coordinates.

If a is allocated on the stack, say of type (array point 6) at (ESP+4):

  offset/EAX : (offset point) <- index i, 8  # (22)
  tmp/EBX : (address point) <- advance a : (array point 6), offset/EAX  # (26)
  tmp2/ECX : (address number) <- get tmp/EBX : (address point), 4/y  # (18)
  *tmp2/ECX <- copy 4  # (5 for copy/mov with 0 disp8)

Many instructions, particularly variants of 'get' and 'advance' -- end up encoding the exact same instructions.
But the types differ, and the type-checker checks them differently.

=== Advanced checks

Couple of items require inserting mapping to multiple instructions:
  bounds checking against array length in 'advance'
  dereferencing 'ref' types (see type list up top)

A. Dereferencing a ref

    tmp/EDX <- advance *s, tmp0/EDI
      => compare (ESP+4), *(ESP+8)  ; '*' from compiler2
         jump-unless-equal panic
         EDX <- add ESP, 8
         EDX <- copy *EDX
         EDX <- add EDX, 4
         EDX <- 8d/lea EDX + result

=== More speculative ideas

Initialize data segment with special extensible syntax for literals. All
literals except numbers and strings start with %.

  %size(type) => compiler replaces with size of type
  %point(3, 4) => two words

and so on.

=== Credits

Forth
C
Rust
Lisp
qhasm
pan>p:&:point -> p:&:point [ local-scope load-ingredients *p <- put *p, x:offset, 34 ] $error: 0 :(scenario can_modify_ingredients_that_are_also_products_3) def main [ local-scope p:&:@:num <- new number:type, 3 p <- foo p ] # mutable address def foo p:&:@:num -> p:&:@:num [ local-scope load-ingredients *p <- put-index *p, 0, 34 ] $error: 0 :(scenario ignore_literal_ingredients_for_immutability_checks) def main [ local-scope p:&:d1 <- new d1:type q:num <- foo p ] def foo p:&:d1 -> q:num [ local-scope load-ingredients x:&:d1 <- new d1:type *x <- put *x, p:offset, 34 # ignore this 'p' return 36 ] container d1 [ p:num q:num ] $error: 0 :(scenario cannot_modify_immutable_ingredients) % Hide_errors = true; def main [ local-scope x:&:num <- new number:type foo x ] # immutable address to primitive def foo x:&:num [ local-scope load-ingredients *x <- copy 34 ] +error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product :(scenario cannot_modify_immutable_containers) % Hide_errors = true; def main [ local-scope x:point-number <- merge 34, 35, 36 foo x ] # immutable container def foo x:point-number [ local-scope load-ingredients # copy an element: ok y:point <- get x, xy:offset # modify the element: boom # This could be ok if y contains no addresses, but we're not going to try to be that smart. # It also makes the rules easier to reason about. If it's just an ingredient, just don't try to change it. y <- put y, x:offset, 37 ] +error: foo: cannot modify 'y' in instruction 'y <- put y, x:offset, 37' because that would modify 'x' which is an ingredient of recipe foo but not also a product :(scenario can_modify_immutable_pointers) def main [ local-scope x:&:num <- new number:type foo x ] def foo x:&:num [ local-scope load-ingredients # modify the address, not the payload x <- copy 0 ] $error: 0 :(scenario can_modify_immutable_pointers_but_not_their_payloads) % Hide_errors = true; def main [ local-scope x:&:num <- new number:type foo x ] def foo x:&:num [ local-scope load-ingredients # modify address; ok x <- new number:type # modify payload: boom # this could be ok, but we're not going to try to be that smart *x <- copy 34 ] +error: foo: cannot modify 'x' in instruction '*x <- copy 34' because it's an ingredient of recipe foo but not also a product :(scenario cannot_call_mutating_recipes_on_immutable_ingredients) % Hide_errors = true; def main [ local-scope p:&:point <- new point:type foo p ] def foo p:&:point [ local-scope load-ingredients bar p ] def bar p:&:point -> p:&:point [ local-scope load-ingredients # p could be modified here, but it doesn't have to be, it's already marked # mutable in the header ] +error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product :(scenario cannot_modify_copies_of_immutable_ingredients) % Hide_errors = true; def main [ local-scope p:&:point <- new point:type foo p ] def foo p:&:point [ local-scope load-ingredients q:&:point <- copy p *q <- put *q, x:offset, 34 ] +error: foo: cannot modify 'q' in instruction '*q <- put *q, x:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product :(scenario can_modify_copies_of_mutable_ingredients) def main [ local-scope p:&:point <- new point:type foo p ] def foo p:&:point -> p:&:point [ local-scope load-ingredients q:&:point <- copy p *q <- put *q, x:offset, 34 ] $error: 0 :(scenario cannot_modify_address_inside_immutable_ingredients) % Hide_errors = true; container foo [ x:&:@:num # contains an address ] def main [ # don't run anything ] def foo a:&:foo [ local-scope load-ingredients x:&:@:num <- get *a, x:offset # just a regular get of the container *x <- put-index *x, 0, 34 # but then a put-index on the result ] +error: foo: cannot modify 'x' in instruction '*x <- put-index *x, 0, 34' because that would modify a which is an ingredient of recipe foo but not also a product :(scenario cannot_modify_address_inside_immutable_ingredients_2) container foo [ x:&:@:num # contains an address ] def main [ # don't run anything ] def foo a:&:foo [ local-scope load-ingredients b:foo <- merge 0 # modify b, completely unrelated to immutable ingredient a x:&:@:num <- get b, x:offset *x <- put-index *x, 0, 34 ] $error: 0 :(scenario cannot_modify_address_inside_immutable_ingredients_3) % Hide_errors = true; def main [ # don't run anything ] def foo a:&:@:&:num [ local-scope load-ingredients x:&:num <- index *a, 0 # just a regular index of the array *x <- copy 34 # but then modify the result ] +error: foo: cannot modify 'x' in instruction '*x <- copy 34' because that would modify a which is an ingredient of recipe foo but not also a product :(scenario cannot_modify_address_inside_immutable_ingredients_4) def main [ # don't run anything ] def foo a:&:@:&:num [ local-scope load-ingredients b:&:@:&:num <- new {(address number): type}, 3 # modify b, completely unrelated to immutable ingredient a x:&:num <- index *b, 0 *x <- copy 34 ] $error: 0 :(scenario latter_ingredient_of_index_is_immutable) def main [ # don't run anything ] def foo a:&:@:&:@:num, b:num -> a:&:@:&:@:num [ local-scope load-ingredients x:&:@:num <- index *a, b *x <- put-index *x, 0, 34 ] $error: 0 :(scenario can_traverse_immutable_ingredients) container test-list [ next:&:test-list ] def main [ local-scope p:&:test-list <- new test-list:type foo p ] def foo p:&:test-list [ local-scope load-ingredients p2:&:test-list <- bar p ] def bar x:&:test-list -> y:&:test-list [ local-scope load-ingredients y <- get *x, next:offset ] $error: 0 :(scenario treat_optional_ingredients_as_mutable) def main [ k:&:num <- new number:type test k ] # recipe taking an immutable address ingredient def test k:&:num [ local-scope load-ingredients foo k ] # ..calling a recipe with an optional address ingredient def foo -> [ local-scope load-ingredients k:&:num, found?:bool <- next-ingredient # we don't further check k for immutability, but assume it's mutable ] $error: 0 :(scenario treat_optional_ingredients_as_mutable_2) % Hide_errors = true; def main [ local-scope p:&:point <- new point:type foo p ] def foo p:&:point [ local-scope load-ingredients bar p ] def bar [ local-scope load-ingredients p:&:point <- next-ingredient # optional ingredient; assumed to be mutable ] +error: foo: cannot modify 'p' in instruction 'bar p' because it's an ingredient of recipe foo but not also a product //: when checking for immutable ingredients, remember to take space into account :(scenario check_space_of_reagents_in_immutability_checks) def main [ a:space <- new-closure b:&:num <- new number:type run-closure b:&:num, a:space ] def new-closure [ new-default-space x:&:num <- new number:type return default-space ] def run-closure x:&:num, s:space [ local-scope load-ingredients 0:space/names:new-closure <- copy s # different space; always mutable *x:&:num/space:1 <- copy 34 ] $error: 0 :(before "End Transforms") Transform.push_back(check_immutable_ingredients); // idempotent :(code) void check_immutable_ingredients(const recipe_ordinal r) { // to ensure an address reagent isn't modified, it suffices to show that // a) we never write to its contents directly, // b) we never call 'put' or 'put-index' on it, and // c) any non-primitive recipe calls in the body aren't returning it as a product const recipe& caller = get(Recipe, r); trace(9991, "transform") << "--- check mutability of ingredients in recipe " << caller.name << end(); if (!caller.has_header) return; // skip check for old-style recipes calling next-ingredient directly for (int i = 0; i < SIZE(caller.ingredients); ++i) { const reagent& current_ingredient = caller.ingredients.at(i); if (is_present_in_products(caller, current_ingredient.name)) continue; // not expected to be immutable // End Immutable Ingredients Special-cases set<reagent> immutable_vars; immutable_vars.insert(current_ingredient); for (int i = 0; i < SIZE(caller.steps); ++i) { const instruction& inst = caller.steps.at(i); check_immutable_ingredient_in_instruction(inst, immutable_vars, current_ingredient.name, caller); if (inst.operation == INDEX && SIZE(inst.ingredients) > 1 && inst.ingredients.at(1).name == current_ingredient.name) continue; update_aliases(inst, immutable_vars); } } } void update_aliases(const instruction& inst, set<reagent>& current_ingredient_and_aliases) { set<int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases); if (!contains_key(Recipe, inst.operation)) { // primitive recipe switch (inst.operation) { case COPY: for (set<int>::iterator p = current_ingredient_indices.begin(); p != current_ingredient_indices.end(); ++p) current_ingredient_and_aliases.insert(inst.products.at(*p).name); break; case GET: case INDEX: case MAYBE_CONVERT: // current_ingredient_indices can only have 0 or one value if (!current_ingredient_indices.empty() && !inst.products.empty()) { if (is_mu_address(inst.products.at(0)) || is_mu_container(inst.products.at(0)) || is_mu_exclusive_container(inst.products.at(0))) current_ingredient_and_aliases.insert(inst.products.at(0)); } break; default: break; } } else { // defined recipe set<int> contained_in_product_indices = scan_contained_in_product_indices(inst, current_ingredient_indices); for (set<int>::iterator p = contained_in_product_indices.begin(); p != contained_in_product_indices.end(); ++p) { if (*p < SIZE(inst.products)) current_ingredient_and_aliases.insert(inst.products.at(*p)); } } } set<int> scan_contained_in_product_indices(const instruction& inst, set<int>& ingredient_indices) { set<reagent> selected_ingredients; const recipe& callee = get(Recipe, inst.operation); for (set<int>::iterator p = ingredient_indices.begin(); p != ingredient_indices.end(); ++p) { if (*p >= SIZE(callee.ingredients)) continue; // optional immutable ingredient selected_ingredients.insert(callee.ingredients.at(*p)); } set<int> result; for (int i = 0; i < SIZE(callee.products); ++i) { const reagent& current_product = callee.products.at(i); const string_tree* contained_in_name = property(current_product, "contained-in"); if (contained_in_name && selected_ingredients.find(contained_in_name->value) != selected_ingredients.end()) result.insert(i); } return result; } :(scenarios transform) :(scenario immutability_infects_contained_in_variables) % Hide_errors = true; container test-list [ value:num next:&:test-list ] def main [ local-scope p:&:test-list <- new test-list:type foo p ] def foo p:&:test-list [ # p is immutable local-scope load-ingredients p2:&:test-list <- test-next p # p2 is immutable *p2 <- put *p2, value:offset, 34 ] def test-next x:&:test-list -> y:&:test-list/contained-in:x [ local-scope load-ingredients y <- get *x, next:offset ] +error: foo: cannot modify 'p2' in instruction '*p2 <- put *p2, value:offset, 34' because that would modify p which is an ingredient of recipe foo but not also a product :(code) void check_immutable_ingredient_in_instruction(const instruction& inst, const set<reagent>& current_ingredient_and_aliases, const string& original_ingredient_name, const recipe& caller) { // first check if the instruction is directly modifying something it shouldn't for (int i = 0; i < SIZE(inst.products); ++i) { if (has_property(inst.products.at(i), "lookup") && current_ingredient_and_aliases.find(inst.products.at(i)) != current_ingredient_and_aliases.end()) { string current_product_name = inst.products.at(i).name; if (current_product_name == original_ingredient_name) raise << maybe(caller.name) << "cannot modify '" << current_product_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end(); else raise << maybe(caller.name) << "cannot modify '" << current_product_name << "' in instruction '" << to_original_string(inst) << "' because that would modify " << original_ingredient_name << " which is an ingredient of recipe " << caller.name << " but not also a product\n" << end(); return; } } // check if there's any indirect modification going on set<int> current_ingredient_indices = ingredient_indices(inst, current_ingredient_and_aliases); if (current_ingredient_indices.empty()) return; // ingredient not found in call for (set<int>::iterator p = current_ingredient_indices.begin(); p != current_ingredient_indices.end(); ++p) { const int current_ingredient_index = *p; reagent current_ingredient = inst.ingredients.at(current_ingredient_index); canonize_type(current_ingredient); const string& current_ingredient_name = current_ingredient.name; if (!contains_key(Recipe, inst.operation)) { // primitive recipe // we got here only because we got an instruction with an implicit product, and the instruction didn't explicitly spell it out // put x, y:offset, z // instead of // x <- put x, y:offset, z if (inst.operation == PUT || inst.operation == PUT_INDEX) { if (current_ingredient_index == 0) { if (current_ingredient_name == original_ingredient_name) raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end(); else raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because that would modify '" << original_ingredient_name << "' which is an ingredient of recipe " << caller.name << " but not also a product\n" << end(); } } } else { // defined recipe if (is_modified_in_recipe(inst.operation, current_ingredient_index, caller)) { if (current_ingredient_name == original_ingredient_name) raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because it's an ingredient of recipe " << caller.name << " but not also a product\n" << end(); else raise << maybe(caller.name) << "cannot modify '" << current_ingredient_name << "' in instruction '" << to_original_string(inst) << "' because that would modify '" << original_ingredient_name << "' which is an ingredient of recipe " << caller.name << " but not also a product\n" << end(); } } } } bool is_modified_in_recipe(const recipe_ordinal r, const int ingredient_index, const recipe& caller) { const recipe& callee = get(Recipe, r); if (!callee.has_header) { raise << maybe(caller.name) << "can't check mutability of ingredients in recipe " << callee.name << " because it uses 'next-ingredient' directly, rather than a recipe header.\n" << end(); return true; } if (ingredient_index >= SIZE(callee.ingredients)) return false; // optional immutable ingredient return is_present_in_products(callee, callee.ingredients.at(ingredient_index).name); } bool is_present_in_products(const recipe& callee, const string& ingredient_name) { for (int i = 0; i < SIZE(callee.products); ++i) { if (callee.products.at(i).name == ingredient_name) return true; } return false; } set<int> ingredient_indices(const instruction& inst, const set<reagent>& ingredient_names) { set<int> result; for (int i = 0; i < SIZE(inst.ingredients); ++i) { if (is_literal(inst.ingredients.at(i))) continue; if (ingredient_names.find(inst.ingredients.at(i)) != ingredient_names.end()) result.insert(i); } return result; } //: Sometimes you want to pass in two addresses, one pointing inside the //: other. For example, you want to delete a node from a linked list. You //: can't pass both pointers back out, because if a caller tries to make both //: identical then you can't tell which value will be written on the way out. //: //: Experimental solution: just tell Mu that one points inside the other. //: This way we can return just one pointer as high up as necessary to capture //: all modifications performed by a recipe. //: //: We'll see if we end up wanting to abuse /contained-in for other reasons. :(scenarios transform) :(scenario can_modify_contained_in_addresses) container test-list [ value:num next:&:test-list ] def main [ local-scope p:&:test-list <- new test-list:type foo p ] def foo p:&:test-list -> p:&:test-list [ local-scope load-ingredients p2:&:test-list <- test-next p p <- test-remove p2, p ] def test-next x:&:test-list -> y:&:test-list [ local-scope load-ingredients y <- get *x, next:offset ] def test-remove x:&:test-list/contained-in:from, from:&:test-list -> from:&:test-list [ local-scope load-ingredients *x <- put *x, value:offset, 34 # can modify x ] $error: 0 :(before "End Immutable Ingredients Special-cases") if (has_property(current_ingredient, "contained-in")) { const string_tree* tmp = property(current_ingredient, "contained-in"); if (!tmp->atom || (!is_present_in_ingredients(caller, tmp->value) && !is_present_in_products(caller, tmp->value))) { raise << maybe(caller.name) << "/contained-in can only point to another ingredient or product, but got '" << to_string(property(current_ingredient, "contained-in")) << "'\n" << end(); } continue; } :(scenario contained_in_check) container test-list [ value:num next:&:test-list ] def test-remove x:&:test-list/contained-in:result, from:&:test-list -> result:&:test-list [ local-scope load-ingredients result <- copy 0 ] $error: 0