8. Dolphin – Phase 5#

Attention

This is a group assignment. The workload is calibrated for a group of 3. Please also see the recommended workflow section in the Appendix below.

In case of questions regarding ambiguity of what you should do, ask questions on the forum. If you are in doubt and there is no enough time, use your best judgment and explain your reasoning in your report.

8.1. Assignment overview#

This assignment extends the language with three language features: records, arrays, and strings. For examples of programs that use these features, we refer to the Exercises for week 7, and the example programs provided with this assignment.

There are 6 tasks and no questions in this assignment. There is one glory task. Do it only if you have the time and feel ambitious.

8.1.1. What you need to get started#

8.1.2. What you need to hand in#

Please hand in a .zip file containing the following.

  1. A brief report documenting your solution. Acceptable report formats are .pdf, .rtf, and .md. For each task and question, briefly (1 – 4 sentences) describe your implementation and answer. Write concisely.

  2. All the source files needed to reproduce your solution. This also includes the C code provided. Please explain in your report how the solution could be reproduced, e.g., calling make (if you have created a Makefile), the command line to call clang, etc.

  3. All the tests that you create (see Task 6) should be placed into a directory assignment-08-tests as individual .dlp files.

Important

Make sure to understand all the code you hand in.

8.2. Records in Dolphin#

Records in Dolphin are similar to structs in C (or Java classes without methods). They are the only form of user-defined data types. Records allow grouping data of different types under a single entity. Record types are declared using the record keyword, which has the following syntax:

record <record-type-name> {
  <field-name-1>:<type-1>;
  <field-name-2>:<type-2>;
  ...
  <field-name-n>:<type-n>;
}

For example, record Tuple { x: int; y : int; }. A record type is referred to in the program using its name, for example via var x:MyRec.

New records are declared using the syntax

new <record-type-name> {
  <field-name-1> = <field-init-expression-1>;
  <field-name-2> = <field-init-expression-2>;
  ...
  <field-name-n> = <field-init-expression-n>;
}

where each <field-init-expressions> must have the type corresponding to the field it initializes.

Given an expression e of record type, its field f is accessed using the dot notation: e.f. The keyword nil denotes an invalid record of any record type, like null in Java; attempting to access one of its fields should be reported as an error at runtime. Below is an example of a simple program with records:

record Tuple { x: int; y : int ; } 

int main () {
    var a:Tuple = new Tuple { x = 0; y = 1; };
    var b:Tuple = nil;
    return a.x;  /* dot notation to refer to the field `x` of variable `a` */
}

The following applies to records and their use in Dolphin programs.

  1. All records are declared at the top-level. A Dolphin program is a collection of function and record declarations.

  2. All records are mutually recursive.

  3. Within a program, all record names must be unique.

  4. The following record names are reserved for the standard library. They may not be used in the declarations of the user-defined records: stream, socket, socket_address, ip_version, accepted_connection, udp_recvfrom_result, and connection_type.

  5. The only way to obtain non-nil values for the reserved records is through standard library. In particular, reserved records cannot be created in the program using the new keyword.

  6. All fields within the record must be unique.

  7. The number of fields may be zero.

  8. At record creation, the fields may appear in any order. For example, var a:Tuple = new Tuple { y = 1; x = 0; }; is a valid record creation.

  9. All fields must be initialized. If a field initialization is missing, an error must be reported.

  10. The only binary operations allowed on the records are equalty and inequality. Records are compared by reference. The following example program illustrates record equality.

/* valid program; returns 1 */
record Tuple { x: int; y : int ; } 

int main () {
    var a:Tuple = new Tuple { x = 0; y = 1;};
    var b:Tuple = new Tuple { x = 0; y = 1;};
    var c = a;
    if (b == c) {
        return 0;
    }
    if (a == c) {
        return 1;
    }   
    return 2;
}
  1. If one of the operands of equality (or inequality) is a record, the other operand must be either (a) another record of the same type, or (b) nil.

Note

This particular design of record comparison is compatible with mainstream languages such as Java and C. This also means that other ways of definging equality, i.e., structurally, have to be implemented in code, e.g., by writing a function bool tuple_eq (Tuple t1, Tuple t2).

8.3. Arrays in Dolphin#

Arrays in Dolphin are similar to arrays in languages such as Java or C. They hold a fixed number of values of a single type. The type of the array is denoted using the syntax [<element-type>], e.g., [int] is the type of array of integers. Arrays are initialized using the syntax new <element-type> [<length-expression>], where <length-expression> must evaluate to an integer. For example, var x = new int[1+3] initializes x to be an array of 4 elements. The length of the arrays is, in general, not known at compile time. Arrays are indexed using the bracket notation <array-expression> [ <index-expression> ].

The following applies to arrays:

  1. All array accesses – reading and writing – are bounds-checked.

  2. Similar to records, the only allowed binary operations on arrays are equality and inequality. Similar to records, the equality checks are done by reference.

  3. The number of array elements must be non-negative.

  4. After array creation, the length of the array cannot change at runtime.

  5. Array length is accessed using length_of (<expression>), where <expression> must evaluate to an array, and length_of is a keyword. (length_of also supports strings; see below.)

8.4. Strings in Dolphin#

In Dolphin, strings are a built-in type. They can be created in one of the following ways:

  • using string literals in the program source, e.g,. "Hello"

  • using Dolphin standard library functions, e.g., string_concat ("Hello", "World") concatenates two strings.

The following applies to strings:

  1. The binary operations on strings are equality, inequality, and string comparison. Unlike arrays and records, strings are compared by value.

  2. String literals may include escape characters: for example ‘\n’ stands for a newline. Dolphin follows OCaml’s lexical convention for representing escape sequences (which can be handled using OCaml’s Scanf.unescaped).

  3. String literals may span over several lines.

  4. The length of a string can be obtained using length_of keyword.

The following example program illustrates some string operations:

int main() {
    var x = "hello
world";
    var y = "hello\nworld";
    if (x == y) {
        return 0;
    }
    return length_of(x);
}

8.5. Abstract Syntax Tree#

Task 1: Design and implement an AST and Typed AST for Phase 5.

As you work on this task, take the following aspects into account.

8.5.1. Type representation#

We suggest the following OCaml data structures for representing bytes, strings, arrays, and records.

(* suggested code snippet to incorporate into your AST *)
type recordname = RecordName of {name : string; loc : Loc.location}
type fieldname = FieldName of {name : string; loc : Loc.location}
type ident = Ident of {name : string; loc : Loc.location}

type typ =
| ...  (* placeholder for previous constructors *)
| Byte of {loc : Loc.location}
| Str of {loc : Loc.location}
| Array of {typ : typ; loc : Loc.location}
| Record of {recordname : recordname}

8.5.2. Program structure#

The structure of the new AST should follow the idea that a program is a list of functions or record declarations. You should create a way of representing record declarations in the AST.

8.5.3. Expressions#

To accommodate the new features, we suggest to extend your expr type by adding constructors for the following:

  • record creation

  • array creation

  • nil expression

  • string values

  • the length_of keyword

8.5.4. Extending Lvals#

In programming languages, lvals, correspond to the program entities that may appear to the left of the assignment statement, hence the use of the letter l in the name. In previous phases, lvals were only identifiers. With the addition of records and arrays, the space of lvals is now richer. It includes indexing into arrays, or accessing a record field, as well as their combination, e.g., f().x[1+g()].y[2].z.

With regards to extending the AST to support lvals, we suggest the following.

type expr = 
...
and lval =
| Var of ident
| Idx of { arr   : expr      
         ; index : expr
	 ; loc   : Loc.location
	 }
| Fld of { rcrd  : expr
         ; field : fieldname (* see declaration of fieldname earlier *)
	 ; loc   : Loc.location
	 } 

8.6. Lexer#

Task 2: Extend the lexer to support the new language features

As you work on this task, recognize what new tokens need to be added to the language. For full support of strings, you may need to add a new lexer rule, in order to properly treat escape characters and strings spanning multiple lines. Note that because Dolphin follows OCaml’s specification of escape characters, we can use the OCaml standard library function Scanf.unescape to recognize escape characters.

8.7. Parser#

Task 3: Extend the parser to support the new language features

To score full points on this task, your parser must not have any shift/reduce or reduce/reduce conflicts. As you work on this task, pay attention to proper parsing of lvals in your parser. Consult the examples and the specification earlier in this section regarding the correct syntax, i.e., the use of semicolons when delimiting fields in records. Write your own tests based on the specification.

8.8. Semantic analysis#

Task 4: Extend the semantic analysis to support the new language features

8.8.1. Record declarations#

In the implementation of semantic analysis, pay special attention to the mutual recursion of records. Similarly to the mutual recursion of functions, use two passes over the record declarations.

  1. Identify all record names, and collect them into a data-structure (a list or a set).

  2. Check each record individually, ensuring that the user-defined fields correspond to valid types.

8.8.2. Checking functions#

Once the record declarations are checked, the information about all the records can be collected into an environment where their names map to their types. This environment should also include the reserved records. Use this resulting environment when type checking function signatures and bodies.

8.9. Code generation#

Task 5: Extend the code generation to support the new language features

We start off by going over the runtime and standard library integration.

8.9.1. Runtime and standard library integration#

Before you proceed with code generation, it is necessary to port your project to the new runtime. The runtime and the standard library are split across several modules, because there is a qualitative distinction between the runtime and the standard library (unlike in the earlier phases), which we explain below. We have the following files.

  1. runtime.c contains the extended core runtime functionality, for operations such as record and array allocation, string equality, reporting null pointer access error, etc. What makes these functions part of the runtime (as opposed to the standard library) is that none of these C functions are exposed to the programmer. It is the compiler’s code generator that embeds calls to them, based on the (typed) AST.

  2. stdlib.c contains Dolphin standard library that includes functions that are user-visible. These include a number of “everyday” functionality, such as functions for string concatenation, printing a string, etc. This is a relatively large module.

  3. runtime.h is a header file that is included from stdlib.c.

These files are to be downloaded from Brightspace.

8.9.1.1. Representation of reserved records#

We map reserved records, e.g., stream to empty LLVM records. This is sufficient because when passing these arguments back and forth to the runtime, we will only use pointers to these records. This also means that we could have represented the reserved records as generic pointers, i.e., i8*; but the empty record approach has an added benefit of tagging the intended use of the signature functions (though the typing guarantees are weak because they can be overcome via casts).

8.9.1.2. Bytes#

In addition, some standard library functions rely on a type byte, which should be translated to an LLVM i8.

8.9.1.3. Declaring external functions and types#

Because your generated LLVM file uses the external functionality provided by the runtime and standard library, it needs to include @declare instructions to let LLVM know of the the function signatures and that they are implemented elsewhere.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The following declarations should be included in the generated LLVM file ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; LLVM struct corresponding to the reserved stream type
%dolphin_record_stream = type {  }

; LLVM struct corresponding to array and string representation
%array_type = type {i64, [0 x i8] }

@dolphin_rc_empty_string = external global %array_type

; signatures for the runtime functions 
; -- not user visible --
declare i64 @compare_strings(%array_type*, %array_type*)
declare i8* @allocate_record(i32)
declare i8* @raw_allocate_on_heap(i32)
declare %array_type* @allocate_array(i32, i64, i8*)
declare void @report_error_array_index_out_of_bounds()
declare void @report_error_nil_access()
declare void @report_error_division_by_zero()

; signatures for the core standard library 
; -- these are user visible --
declare %array_type* @bytes_array_to_string(%array_type*)
declare %array_type* @string_to_bytes_array(%array_type*)
declare i64 @byte_to_int_unsigned(i8)
declare i64 @byte_to_int_signed(i8)
declare i8 @int_to_byte_unsigned(i64)
declare i8 @int_to_byte_signed(i64)
declare i64 @ascii_ord(%array_type*)
declare %array_type* @ascii_chr(i64)
declare %array_type* @string_concat(%array_type*, %array_type*)
declare %array_type* @substring(%array_type*, i64, i64)
declare %array_type* @int_to_string(i64)
declare i64 @string_to_int(%array_type*)
declare i64 @input_byte(%dolphin_record_stream*)
declare i1 @output_byte(i8, %dolphin_record_stream*)
declare %array_type* @input_bytes_array(i64, %dolphin_record_stream*)
declare void @output_bytes_array(%array_type*, %dolphin_record_stream*)
declare void @output_string(%array_type*, %dolphin_record_stream*)
declare i1 @seek_in_file(i64, i1, %dolphin_record_stream*)
declare i64 @pos_in_file(%dolphin_record_stream*)
declare i1 @close_file(%dolphin_record_stream*)
declare i1 @flush_file(%dolphin_record_stream*)
declare i1 @error_in_file(%dolphin_record_stream*)
declare i1 @end_of_file(%dolphin_record_stream*)
declare i64 @get_eof()
declare %dolphin_record_stream* @open_file(%array_type*, %array_type*)
declare %dolphin_record_stream* @get_stdin()
declare %dolphin_record_stream* @get_stderr()
declare %dolphin_record_stream* @get_stdout()
declare %array_type* @get_cmd_args()
declare void @exit(i64)

8.9.2. Records#

8.9.2.1. Record representation#

We represent records using LLVM structs. The translation is quite straightforward and is one-to-one. A Dolphin record with n fields is translated to an LLVM struct with n fields. The types of the LLVM fields should correspond to the Dolphin types. For example, the Dolphin declaration of two types.

record T1 { x: int; t2 : T2;}
record T2 { y: int; t1 : T1;}

is translated to LLVM as follows

%dlp_rec_T2 = type { i64, %dlp_rec_T1* }
%dlp_rec_T1 = type { i64, %dlp_rec_T2* }

Note that the choice of the naming %dlp_rec_T2 here is compiler-chosen; you may chose a different naming convention. What is important is that the code generation phase of the compiler is aware of the mapping between the source and the target types, and that of course the generated LLVM code is valid. The reference implementation uses the prefix dolphin_record_ for this purpose.

8.9.2.2. Nil representation#

Nil record values can be represented as null in LLVM.

8.9.2.3. Record initialization#

Allocation of a record is done through the runtime function allocate_record. This function takes an argument corresponding to the size of the record. Because the size depends on LLVM, we need to implement an architecture-independent way of obtaining the size information. This is accomplished using the GEP size hack[Lat05] as illustrated below. To actually save the payload of the record, we further need to use a combination of GEP and store instructions.

For example, consider the following record creation

var t2 = new T2{y = 10; t1 = t1; }  /* for some previously computed value `t1` */

The LLVM code for it can look as follows (we abbreviate getelementptr as <GEP> for brevity in the listing).

In this example, we need to use bitcast to cast the generic pointer i8* returned by the runtime allocation function into the LLVM type. Note that casts typically are erased in the LLVM to x86 translation; in other words, they have no performance overhead.

; Suppose %var_t1 and %var_t2 are the identifiers 
; that we alloca-ed for the source level t1, t2

; GEP size hack 
%size_ptr = getelementptr %dlp_rec_T2, %dlp_rec_T2* null, i32 1
; Cast ptr to integer
%size = ptrtoint %dlp_rec_T2* %size_ptr to i32    
; Call into the runtime for allocation
%ptr_rt = call i8* @allocate_record (i32 %size)   

; Turn generic pointer into a more specific one, so we can use GEP
%t2_ptr = bitcast i8* %ptr_rt to %dlp_record_T2*

; Access field y of the struct 
%ptr_field_y_of_var_t2 = getelementptr %dlp_rec_T2, %dlp_rec_T2* %t2_ptr, i32 0, i32 0
; Save 10 in the field y
store i64 10, i64* %ptr_field_y_of_var_t2         

; Read from %var_t1
%ptr_t1 = load %dlp_rec_T1*, %dlp_rec_T1** %var_t1 
; Access field t1 of the struct 
%ptr_field_t1_of_var_t2 = getelementptr %dlp_rec_T2, %dlp_rec_T2* %t2_ptr, i32 0, i32 1
; Save in the field t1
store %dlp_rec_T1* %ptr_t1, %dlp_rec_T1** %ptr_field_t1_of_var_t2         

; Save in %var_t2
store %dlp_rec_T2* %t2_ptr %dlp_rec_T2** %var_t2

8.9.2.4. Record access#

To access the record, we need to use the GEP instruction similarly to how it is used in the initialization above.

8.9.3. Array translation#

8.9.3.1. Array representation#

An array is represented as a contiguous block of memory, with the metadata information about the length stored in memory before the content.

┌─────────┬─────────────────────────────────────────────────┐
│ Length  │                  Array contents                 │
└─────────┴─────────────────────────────────────────────────┘
▲         ▲                                                  
│         │                                                  
│         The start of the array's contents
│         
│ 
The length of the array

We will use C99’s Flexible array member feature. That is, we will use the following C struct:

struct array { int64_t len; char contents[]; };

Note how the size of the array is not given. This struct type should be translated to LLVM as follows:

%array_type = type {i64, [0 x i8] }

Arrays are represented as pointers to these structs. That is, on the LLVM side we use %array_type* and on the C side, we use struct array *, see e.g., the type of allocate_array function in runtime.c and in the LLVM code provided above.

8.9.3.2. Array allocation and initialization#

Array allocation and initialization takes place via the function allocate_array in the runtime. Note that the last argument to that function needs to include a pointer to the default initialization value based on the type of the array’s elements as follows:

array element type

default value

int

0

bool

false

string

""

byte

0

[T] (arrays)

nil

record types

nil

8.9.3.3. Array access#

To access the i-th element of the array (counting from zero), after using the GEP instruction to obtain a pointer to the contents of the array (the second field in %array_type), we can use GEP instruction again with i as the first index.

8.9.4. String translation#

8.9.4.1. Runtime representation#

The runtime representation of strings is exactly the same as arrays.

8.9.4.2. String literals in LLVM code.#

String literals are represented in the source as global identifiers.

A string literal is represented as an LLVM global of LLVM array_type. That is, as a struct type with two fields, the first i64 field stores the length, while the second field of type [n x i8], an LLVM array, stores the contents of the string where n is the length of the string literal. Recall that LLVM arrays have static length (and we only used here to represent string literals). Consider the following source program and the associated translation.

int main () {
    var x = "Hello World\n"; 
    output_string (x, get_stdout());
    return 0;
}

The corresponding LLVM code can look as follows

;...

%dolphin_record_stream = type {  }
%array_type = type {i64, [0 x i8] }

;...

@string_literal$1 = global { i64, [12 x i8] } {i64 12, [12 x i8] c"Hello World\0A"}

;...

declare void @output_string(%array_type*, %dolphin_record_stream*)
declare %dolphin_record_stream* @get_stdout()

;...

define i64 @dolphin_fun_main () {
 %x$0 = alloca %array_type*
 %conv_string_literal_packed$2 = bitcast { i64, [12 x i8] }* @string_literal$1 to %array_type*
 store %array_type* %conv_string_literal_packed$2, %array_type** %x$0
 %load_local_var$3 = load %array_type*, %array_type** %x$0
 %call$4 = call %dolphin_record_stream* @get_stdout ()
 call void @output_string (%array_type* %load_local_var$3, %dolphin_record_stream* %call$4)
 ret i64 0
after_return$5:
 unreachable
}

Note how the global string_literal$1 is declared of type { i64, [12 x i8] } but is treated in the code to have the type { i64, [12 x i8] }* — recall that global declarations produce pointers; see Global data definitions. This is why in the code we bitcast it from { i64, [12 x i8] }* into array_type*.

8.9.5. Comparing strings#

String comparison is to be translated to calls to the runtime function compare_strings. This function compares two strings lexicographically. It returns 0 if the strings are identical, -1 if the first string is less than the second one, and 1 if the first string is greater than the second one.

8.10. Consolidation and testing#

Task 6: Put all the phases together and test your functionality

  • Add at least 10 new tests that check for the negative behavior in the semantic analysis and the frontend that you have implemented.

  • Add at least 10 new tests that check for the positive behavior of the frontend.

  • Run your compiler on the provided example programs and check their behavior.

Describe why your tests are useful.

8.10.1. Glory task#

Task 7 (glory): Add support for the full standard library

Full standard library includes networking API. This will allow you to run the HTTP server example (provided among the examples on Brightspace). See full standard library signature in the Appendix.

8.11. Appendix#

8.11.1. Example programs#

We provide a handful of example programs together with their expected output. Download them from brightspace.

8.11.3. Full standard library signature#

%dolphin_record_udp_recvfrom_result = type { %array_type*, %dolphin_record_socket_address* }
%dolphin_record_accepted_connection = type { %dolphin_record_socket*, %dolphin_record_socket_address* }
%dolphin_record_socket = type {  }
%dolphin_record_socket_address = type {  }
%dolphin_record_ip_address = type {  }
%dolphin_record_ip_version = type {  }
%dolphin_record_connection_type = type {  }
%dolphin_record_stream = type {  }
%array_type = type {i64, [0 x i8] }

@dolphin_rc_empty_string = external global %array_type

declare i64 @compare_strings(%array_type*, %array_type*)
declare i8* @allocate_record(i32)
declare i8* @raw_allocate_on_heap(i32)
declare %array_type* @allocate_array(i32, i64, i8*)
declare void @report_error_array_index_out_of_bounds()
declare void @report_error_nil_access()
declare void @report_error_division_by_zero()
declare %dolphin_record_udp_recvfrom_result* @socket_recvfrom_udp(%dolphin_record_socket*)
declare i64 @socket_sendto_udp(%dolphin_record_socket*, %dolphin_record_socket_address*, %array_type*)
declare i1 @socket_close(%dolphin_record_socket*)
declare i1 @socket_activate_udp(%dolphin_record_socket*)
declare i1 @socket_connect(%dolphin_record_socket*, %dolphin_record_socket_address*)
declare %dolphin_record_accepted_connection* @socket_accept(%dolphin_record_socket*)
declare i1 @socket_listen(%dolphin_record_socket*, i64)
declare i1 @socket_bind(%dolphin_record_socket*, %dolphin_record_socket_address*)
declare i64 @get_port_of_socket_address(%dolphin_record_socket_address*)
declare %dolphin_record_ip_address* @get_ip_address_of_socket_address(%dolphin_record_socket_address*)
declare %dolphin_record_socket_address* @create_socket_address(%dolphin_record_ip_address*, i64)
declare %array_type* @ip_address_to_string(%dolphin_record_ip_address*)
declare %dolphin_record_ip_address* @string_to_ip_address(%dolphin_record_ip_version*, %array_type*)
declare %dolphin_record_stream* @socket_get_output_stream(%dolphin_record_socket*)
declare %dolphin_record_stream* @socket_get_input_stream(%dolphin_record_socket*)
declare %dolphin_record_socket* @create_socket(%dolphin_record_ip_version*, %dolphin_record_connection_type*)
declare %dolphin_record_ip_address* @get_ipv6_address_any()
declare %dolphin_record_ip_address* @get_ipv4_address_any()
declare %dolphin_record_ip_version* @get_ipv6()
declare %dolphin_record_ip_version* @get_ipv4()
declare %dolphin_record_connection_type* @get_tcp_connection_type()
declare %dolphin_record_connection_type* @get_udp_connection_type()
declare %array_type* @bytes_array_to_string(%array_type*)
declare %array_type* @string_to_bytes_array(%array_type*)
declare i64 @byte_to_int_unsigned(i8)
declare i64 @byte_to_int_signed(i8)
declare i8 @int_to_byte_unsigned(i64)
declare i8 @int_to_byte_signed(i64)
declare i64 @ascii_ord(%array_type*)
declare %array_type* @ascii_chr(i64)
declare %array_type* @string_concat(%array_type*, %array_type*)
declare %array_type* @substring(%array_type*, i64, i64)
declare %array_type* @int_to_string(i64)
declare i64 @string_to_int(%array_type*)
declare i64 @input_byte(%dolphin_record_stream*)
declare i1 @output_byte(i8, %dolphin_record_stream*)
declare %array_type* @input_bytes_array(i64, %dolphin_record_stream*)
declare void @output_bytes_array(%array_type*, %dolphin_record_stream*)
declare void @output_string(%array_type*, %dolphin_record_stream*)
declare i1 @seek_in_file(i64, i1, %dolphin_record_stream*)
declare i64 @pos_in_file(%dolphin_record_stream*)
declare i1 @close_file(%dolphin_record_stream*)
declare i1 @flush_file(%dolphin_record_stream*)
declare i1 @error_in_file(%dolphin_record_stream*)
declare i1 @end_of_file(%dolphin_record_stream*)
declare i64 @get_eof()
declare %dolphin_record_stream* @open_file(%array_type*, %array_type*)
declare %dolphin_record_stream* @get_stdin()
declare %dolphin_record_stream* @get_stderr()
declare %dolphin_record_stream* @get_stdout()
declare %array_type* @get_cmd_args()
declare void @exit(i64)