The IC Language Specification

For the CS434 project, you will build a compiler for an object-oriented language called IC (for Iced Coffee), which is essentially a simplified version of Java. Despite all of the simplifications allowing us to have a one-page grammar specification, this language preserves enough Java constructs and features to make this project challenging and to let us write many interesting programs. In the rest of this document we give a detailed description of the language, discussing its syntax and semantics.

IC, and some of the related assignments and homeworks, is based on similar assignments developed at Cornell.

Overview

The following list highlights the main features of the language:

Lexical Considerations

Identifiers and keywords are case-sensitive. Identifiers must begin with an alphabetic character. Following the initial alphabetic character may be any sequence of alphabetic characters, numeric characters, or the underscore character (_). Uppercase and lowercase alphabetic characters are both considered alphabetic and are distinguished, so x and X are different identifiers. For simplicity, you may assume that class names always start with a capital letter and all other identifiers start with lower-case letters.

The following are the keywords in the language and cannot be used as identifiers:

\[ \begin{array}{lllllll} \tt class & \tt extends & \tt Library & \tt void & \tt int & \tt boolean & \tt string \\ \tt return & \tt if & \tt else & \tt while & \tt break & \tt continue & \tt this \\ \tt new & \tt length & \tt true & \tt false & \tt null \end{array}\]

White spaces consists of a sequence of one or more space, tab, or newline characters. White spaces may appear between any tokens. Keywords and identifiers must be separated by white space or a token that is neither a keyword or an identifier. For instance, elsex represents a single identifier, not the keyword else followed by the identifier x.

Both forms of Java comments are supported; a comment beginning with the characters // indicates that the remainder of the line is a comment. In addition, a comment can be a sequence of characters that begins with /*, followed by any characters, including newline, up to the first occurrence of the end sequence */. Unclosed comments are lexical errors.

Integer literals are unsigned sequences of digits; the character - is always lexed as a separate operator token, never as part of an integer literal. Non-zero numbers should not have leading zeroes. Integers have 32-bit signed values between \(-2^{31}\) and \(2^{31}-1\). As a special case, the literal 2147483648 (that is, \(2^{31}\)) is permitted only as the immediate operand of a unary minus, so that -2147483648 is a legal expression; anywhere else, 2147483648 is a range error.

String literals are sequences of characters enclosed in double quotes. String characters can be:

  1. printable ASCII characters (ASCII codes between decimal 32 and

    126) other than quote " and backslash \, and

  2. the escape sequences \" to denote quote, \\ to denote backslash, \t to denote tab, and \n for newline.

No other characters or character sequences can occur in a string. Unclosed strings are lexical errors.

Boolean literals must be one of the keywords true or false. The only literal for heap references is null.

Program Structure

A program consists of a sequence of class declarations. Each class in the program contains field and method declarations. A program must have exactly one method main, with the following signature:

  void main (string[] args) { ... } 

In contrast to Java, the main method is dynamic. That is, if main belongs to a class A, the execution of the program consists of creating a new object of class A, then invoking its main method. In fact, all of the method calls (except library function calls) are dynamic, which simplifies typechecking in this language.

Variables

Program variables may be local variables or parameters of methods, in which case they are allocated on stack; or they may be fields of objects or array elements, allocated on heap. Program variables of type int and boolean hold integer and boolean values, respectively. Variables of other types contain references to heap-allocated structures (i.e., strings, arrays, or objects).

The program does not initialize variables by default when they are declared. Instead, the compiler statically checks that each variable is guaranteed to have a value assigned before being used. Object fields and array elements are initialized with default values (0 for integers, false for booleans, and null for references) when such structures are dynamically created.

The language allows variables to be initialized when declared at the beginning of a statement block. The initialization expression can refer to variables in enclosing scopes (or to the formal arguments of the enclosing method), but cannot involve any of the variables declared in the current block.

Strings

For string references, the language uses the primitive type string (unlike Java, where String is a class). Strings are allocated on heap and are immutable, which means that the program cannot modify their contents. The language allows only the following operations on string variables: assignments of string references (including null); concatenating strings with the + operator; and testing for string reference equality using == and != (Note: this operator does not compare string contents). Library functions are provided to convert integers to strings, etc.

Arrays

The language supports arrays with arbitrary element types. If T is a type, then T[] is the type for an array with elements of type T. In particular, array elements can be arrays themselves, allowing programmers to build multidimensional arrays. For instance, the type T[][] describes a two-dimensional array constructed as an array of array references T[].

Arrays are dynamically created using the new construct: new T[n] allocates an array of type T with n elements and initializes the elements with their default values. The expression new T[n] yields reference to the newly created array. Arrays of size \(n\) are indexed from 0 to \(n-1\) and the standard bracket notation is used to access array elements. If the expression a is a reference to an array of length \(n\), then a.length is \(n\), and a[i] returns the (i+1)-th element in the array. Note that length is a keyword, so no field, method, or variable may be named length. For each array access a[i], the program checks at run-time that a is not null and that the access is within bounds: \(\tt 0 \leq i < n\). Violations will terminate the program with an error message.

Classes

Classes are collections of fields and methods. They are defined using declarations of the form:

class A extends B { body }

where body is a sequence of field and method declarations. The extends clause is optional. When the extends clause is present, class A inherits all of the features (methods and fields) of class B. Only one class can be inherited. Hence IC supports only single inheritance. We say that A is a subclass of B, and that B is a superclass of A.

Classes can only extend previously defined classes. In other words, a class cannot extend itself or another class defined later in the program. This ensures that the class hierarchy has a tree structure.

Method overloading is not supported: a class cannot have multiple methods with the same name, even if the methods have different number of types of arguments, or different return types. Hidden fields are not permitted either: for each class declaration class B extends A, all of the newly defined fields of B must have different names than the inherited fields from A. Otherwise, those inherited fields couldn’t be accessed by the methods of B.

However, methods can be overridden in subclasses. Subclasses can re-define more specialized versions of methods defined in their superclasses.

Subtyping

Inheritance induces a subtyping relation. When class A extends class B, A is a subtype of B, written \(\tt A \leq B\). Subtyping is also reflexive and transitive. Additionally, the special type Null is a subtype of any reference type:

\[ \Rule{\tt A ~extends ~B ~\{ ... ~\}}{\tt A \leq B} \qquad \Rule{}{\tt A \leq A} \qquad \Rule{\tt A \leq B \quad B \leq C}{\tt A \leq C} \] \[ \Rule{}{{\sf Null} \leq \tt A}\qquad \Rule{}{{\sf Null} \leq \tt T[]}\qquad \Rule{}{{\sf Null} \leq \tt string} \]

If A is a subtype of B, a value of type A can be used in the program whenever the program expects a value of type B.

Subtyping is not covariant for array types: if A is a subtype of B then A[] is not a subtype of B[]. Instead, array subtyping is type invariant, which means that each array type is only a subtype of itself.

As in Java, subtyping for virtual methods is type invariant, both for the parameters and for the return value. This means that each virtual method in a subclass must have the same number and type of arguments, and the same return type as the corresponding methods from the superclasses.

Objects

Objects of a certain class can be dynamically created using the new construct. If A is a declared class, new A() allocates an object of class A on heap and initializes all of its fields with the default values. The expression new A() yields a reference to the newly allocated object.

Object fields and instance methods are accessed using the ‘.’ symbol. The expression o.f denotes the field f of object o, and the expression o.m() denotes a call to the virtual method m of object o. The keyword this refers to the current object (i.e., the object on which the current method is invoked).

For virtual methods, the actual method being invoked by o.m() cannot be determined statically, because the precise type of receiver object o is unknown – it can be the declared class for o or any of its subclasses. Virtual method calls are resolved at run-time via dynamic dispatch.

Object references have class types: each definition class A introduces a class type A. Class types can then be used in declarations for reference variables. For instance, “A obj” declares a variable obj of type A, that is, a reference to an object of class A.

A class name A can be used as the type of an object reference anywhere in the program. In particular, it can appear in the body of the class A declaration itself, or even before that, as in the following example. This allows to build recursive and mutually recursive class structures, such as the ones below:

  class List { int data; List next; } 
  class Node { int data; Edge[] edges; } 
  class Edge { int label; Node dest; } 

Method Invocation

A method invocation consists of the following steps: passing the parameter values from the caller to the callee, executing the body of the callee, and returning the control and the result value (if any) to the caller.

At each method invocation site, the program evaluates the expressions representing the actual arguments and then assigns the computed values to the corresponding formal parameters of the method. Object, array, or string arguments are passed as references to such structures. The arguments are always evaluated from left to right.

After parameters are assigned values, the program executed the body of the invoked method. When the execution reaches a return statement or reaches the end of the method body, the program transfers the control back to the caller. If the return statement has an expression argument, that argument is evaluated and the computed value is also returned to the caller.

At each method invocation, the number and types of actual values of the call site must be the same as the number and types of formal parameters in the method declaration.

Also, the return type from the declaration of a method must match the return statements in the body of that method. More precisely, if a method is declared to return void, then return statements in the method body should have no return expression. In this case, the method is allowed to reach the end its body without encountering a return statement. Otherwise, if the method is declared with a return type T, then return statements must have a return value of type T. In this case, the method body is required to execute a return statement before it reaches the end of its body.

Scoping Rules

For each program, there is a hierarchy of scopes consisting of: the global scope, the class scopes, the method scopes, and the local scopes for blocks within each method. The global scope consists of the names of all classes defined in the program. The scope of a class is the set of fields and methods of that class. The scopes of subclasses are nested inside the scopes of their superclasses. The scope of a method consists of the formal parameters and local variables defined in the block representing the body of the method. Finally, a block scope contains all of the variables defined at the beginning of that block. When resolving an identifier at a certain point in the program, the enclosing scopes are searched for that identifier.

There are a couple of scope rules. First, identifiers can only be used if they are defined in one of the enclosing scopes. More precisely, variables can only be used (read or written) after they are defined in one of the enclosing block or method scopes. Fields and methods can be used in expressions of the form expr.f or expr.m when the object designator expr has class type T and the scope of T contains those fields and methods. This means that all methods and fields are public and can be accessed by other classes. Finally, class names can be used anywhere, provided they are defined in the program (either before or after the point where they are referred to). Fields and methods that are defined in the enclosing class can also be acceessed simply as f or m, in which case the object designator this is implicit.

Another rule is that identifiers (classes, fields, methods, and variables) cannot be defined multiple times in the same scope, unless they are of different kinds (that is, a field and a method of the same class can have the same name). Also, fields with the same names cannot be re-defined in nested class scopes (i.e., in subclasses). Otherwise, identifiers can be defined multiple times in different, possibly nested, scopes. For variables, inner scopes shadow outer scopes. That is, if variables with the same name occur in nested scopes, then each occurence of that variable name refers to the variable in the innermost scope. Finally, it is not allowed to shadow method parameters — the local variables must have different names than the parameters of the enclosing method.

The following examples illustrate some aspects of these scoping rules:

Statements

IC has the standard control statements: assignments, method calls, return statements, if constructs, while loops, break and continue statements, and statement blocks.

Each assignment statement l = e updates the location represented by l with the value of expression e. The updated location l can be a local variable, a parameter, a field, or an array element. The type of the updated location must match the type of the evaluated expression. For integers and booleans, the assignment copies the integer or boolean value. For string, array, or object types, the assignment only copies the reference.

Method invocations can be used as statements, regardless of whether they return values or not. If the invoked method returns a value, that value is discarded.

The if statement has the standard semantics. It first evaluates the test expression, and executes one of its branches depending on whether the test is true of false. The else clause of an if statement always refers to the innermost enclosing if.

The while statement executes its body iteratively. At each iteration, it evaluates the test condition. If the condition is false, then it finishes the execution of the loop; otherwise it executes the loop body and continues with the next iteration. The break and continue statements must occur in the body of an enclosing loop in the current method. The break statement terminates the loop and the execution continues with the next statement after the loop body. The continue statement terminates the current loop iteration; the execution of the program proceeds to the next iteration and tests the loop condition. When break and continue statements occur in nested loops, they refer to the innermost loop.

Blocks of statements consist of a sequences of statements and variable declarations. Blocks are statements themselves, so blocks and statements can be nested arbitrarily deep.

Expressions

Program expressions include:

Operators

Unary and binary operators include the following:

The operator precedence and associativity is defined by the table below. Here, priority 1 is the highest, and priority 9 is the lowest.

Priority Operator Description Associativity
1 []  () array index, method call left
. field/method access
2 -  ! unary minus, logical negation right
3 * / % multiplication, division, remainder left
4 +  - addition, subtraction left
5 < <= > >= relational operators left
6 == != equality comparison left
7 && short-circuit and left
8 || short-circuit or left
9 = assignment right

IC Syntax

The language syntax is show in the figure below. Here, keywords are shown using typewriter fonts (e.g., while); operators and punctuation symbols are shown using single quotes (e.g., ‘;’); the other terminals are written using small caps fonts (Id, ClassId, Integer, and String); and nonterminals using slanted fonts (e.g., formals). The remaining symbols are meta-characters: a raised star, \(\kstar{(\ldots)}\), denotes the Kleene closure of the group, and \(\optional{\;\ldots\;}\) denotes an optional sequence of symbols. So the raised stars below are the grammar’s repetition operator, while IC’s multiplication operator sits on the baseline as ‘*’.

\[\begin{array}{rcl} \nt{program} &::=& \kstar{\nt{classDecl}} \\ \nt{classDecl} &::=& \keyword{class} ~ \cid ~~ \optional{\keyword{extends} ~ \cid} ~~ \lit{\{} ~~ \kstar{\group{\choice{\nt{fieldDecl}}{\nt{methodDecl}}}} ~~ \lit{\}} \\ \nt{fieldDecl} &::=& \nt{type} ~ \id ~~ \kstar{\group{\lit{,} ~\, \id}} ~\,\lit{;} \\ \nt{methodDecl} &::=& \group{\choice{\nt{type}}{\keyword{void}}} ~~ \id ~~ \lit{(}~ \optional{\nt{formals}} ~\lit{)} ~~ \nt{block} \\ \nt{formals} &::=& \nt{type} ~\id ~~ \kstar{\group{\lit{,}~\,\nt{type}~\id}} \\ \\ \nt{type} &::=& \keyword{int} \bnf \keyword{boolean} \bnf \keyword{string} \bnf \cid \bnf \nt{type} ~ \lit{[} ~ \lit{]}\\ \\ \nt{block} &::=& \lit{\{} ~ \kstar{\nt{varDecl}} ~~ \kstar{\nt{stmt}} ~ \lit{\}} \\ \nt{varDecl} &::=& \nt{type} ~~ \id ~ \optional{\lit{=} ~ \nt{expr}} ~~ \kstar{\group{\lit{,} ~ \id ~ \optional{\lit{=} ~ \nt{expr}}}} ~~\lit{;} \\ \\ \nt{stmt} &::=& \nt{location} ~\lit{=} ~\nt{expr} ~~\lit{;}\\ &|& \nt{call} ~~\lit{;}\\ &|& \keyword{return} ~ \optional{\nt{expr}} ~\,\lit{;} \\ &|& \keyword{if} ~ \lit{(}~ \nt{expr} ~\lit{)} ~ \nt{stmt}~~ \optional{\keyword{else} ~ \nt{stmt}} \\ &|& \keyword{while} ~ \lit{(}~ \nt{expr} ~\lit{)} ~ \nt{stmt} \\ &|& \keyword{break} ~\,\lit{;} \\ &|& \keyword{continue} ~\,\lit{;} \\ &|& \nt{block} \\ \\ \nt{expr} &::=& \nt{location} \\ &|& \nt{call} \\ &|& \keyword{this} \\ &|& \keyword{new} ~ \cid ~ \lit{(} ~ \lit{)} \\ &|& \keyword{new} ~ \nt{type} ~ \lit{[} ~ \nt{expr} ~ \lit{]} \\ &|& \nt{expr} ~\lit{.} ~ \keyword{length} \\ &|& \nt{expr} ~\nt{binop} ~ \nt{expr} \\ &|& \nt{unop} ~ \nt{expr} \\ &|& \nt{literal} \\ &|& \lit{(} ~ \nt{expr} ~ \lit{)} \\ \\ \nt{call} &::=& \nt{libCall} ~ \bnf \nt{virtualCall} \\ \nt{libCall} &::=& \keyword{Library} ~\lit{.} ~\id ~~\lit{(} ~~\optional{\nt{expr} ~~ \kstar{\group{\lit{,} ~\, \nt{expr}}}} ~~\lit{)} \\ \nt{virtualCall} &::=& \optional{\nt{expr} ~\lit{.}} ~ \id ~~\lit{(} ~~\optional{\nt{expr} ~~ \kstar{\group{\lit{,} ~\, \nt{expr}}}} ~~\lit{)} \\ \nt{location} &::=& \id \bnf \nt{expr} ~ \lit{.} ~ \id \bnf \nt{expr} ~ \lit{[} ~ \nt{expr} ~ \lit{]} \\ \\ \nt{binop} &::=& \lit{+} \bnf \lit{-} \bnf \lit{*} \bnf \lit{/} \bnf \lit{%} \bnf \lit{&&} \bnf \lit{||}\\ &|& '\texttt{<}' \bnf '\texttt{<=}' \bnf '\texttt{>}' \bnf '\texttt{>=}' \bnf \lit{==} \bnf \lit{!=} \\ \nt{unop} &::=& \lit{-} \bnf \lit{!} \\ \nt{literal} &::=& \integerlit \bnf \stringlit \bnf \keyword{true} \bnf \keyword{false} \bnf\keyword{null} \\ \end{array}\]

Appendix: An LL(1) Form of the Grammar

The grammar above is written for readability: it is ambiguous (binary operators carry their precedence and associativity in prose, in the Operators section) and freely left-recursive. The parser you build in PA 2 needs an LL(1) grammar — no left recursion, and every choice decidable with one token of lookahead. This appendix works several of the required transformations; two are deliberately left for you.

Expressions. Precedence and associativity become one rule per level, from lowest to highest — ||, &&, equality (== !=), relational (< <= > >=), additive (+ -), multiplicative (* / %), unary (- !), postfix — each an iteration \(X \rightarrow Y ~ \kstar{\group{op ~ Y}}\) folded leftward (leftAssoc), so \(9-2-3\) still groups as \((9-2)-3\). The left-recursive postfix forms (\(\nt{expr}~\lit{.}~\id\), \(\nt{expr}~\lit{[}\,\nt{expr}\,\lit{]}\), \(\nt{expr}~\lit{.}~\keyword{length}\), and the method-call form) become one postfix level:

\[\begin{array}{rcl} \nt{postfix} &::=& \nt{primary} ~ \kstar{\nt{postfixOp}} \\ \nt{postfixOp} &::=& \lit{.} ~ \nt{dotTail} ~\bnf~ \lit{[} ~ \nt{expr} ~ \lit{]} \\ \nt{dotTail} &::=& \keyword{length} ~\bnf~ \id ~ \optional{\lit{(} ~ \nt{args} ~ \lit{)}} \\ \end{array}\]

Types. The left-recursive array rule becomes iteration: \(\nt{type} ::= \nt{typeBase} ~ \kstar{\group{\lit{[}~\lit{]}}}\) with \(\nt{typeBase} ::= \keyword{int} \bnf \keyword{boolean} \bnf \keyword{string} \bnf \cid\).

Class members (worked factoring). A field and a non-void method both begin \(\nt{type}~\id\), so one token cannot choose between the grammar’s \(\nt{fieldDecl}\) and \(\nt{methodDecl}\). Factor the common prefix and decide at the next token — ( starts a method, , or ; continues a field:

\[\begin{array}{rcl} \nt{member} &::=& \keyword{void} ~ \id ~ \nt{methodRest} ~\bnf~ \nt{type} ~ \id ~ \nt{memberRest} \\ \nt{memberRest} &::=& \nt{methodRest} ~\bnf~ \kstar{\group{\lit{,}~\id}} ~ \lit{;} \\ \nt{methodRest} &::=& \lit{(} ~ \optional{\nt{formals}} ~ \lit{)} ~ \nt{block} \\ \end{array}\]

Object and array creation (worked factoring). The two \(\keyword{new}\) forms share their first token, and inside an array creation each [ is either an empty dimension pair or the length expression — decided by whether ] follows immediately:

\[\begin{array}{rcl} \nt{newExpr} &::=& \keyword{new} ~ \nt{newTail} \\ \nt{newTail} &::=& \cid ~ \group{\choice{\lit{(}~\lit{)}}{\nt{arraySuffix}}} ~\bnf~ \nt{nonClassBase} ~ \nt{arraySuffix} \\ \nt{arraySuffix} &::=& \lit{[} ~ \nt{bracketRest} \\ \nt{bracketRest} &::=& \lit{]} ~ \nt{arraySuffix} ~\bnf~ \nt{expr} ~ \lit{]} \\ \end{array}\]

(So new int[][10] is an array of ten int[]: dimensions first, one length expression last.)

The dangling else. Factoring the optional else-part yields the classic FIRST/FOLLOW conflict — else can both begin the else-part and follow it. Resolve it greedily (greedyRule): each else binds to the nearest if. This is the appendix’s only intentional conflict.

Deliberately left for you (PA 2). Two factorings are not given here, and they are the interesting ones; both hinge on what an \(\id\) can begin.

The library reports any factoring you get wrong as a FIRST/FIRST or FIRST/FOLLOW conflict naming the productions involved — treat those reports, plus dump(), as your referee.

Appendix: Typing Rules

Typing rules for expressions.

\[\begin{array}{cc} \RuleSide{}{\Env \proves {\tt true} : \Bool}{} & \RuleSide{}{\Env \proves {\tt false} : \Bool}{} \\ \\ \RuleSide{}{\Env \proves \textsl{integer-literal} : \Int}{} & \RuleSide{}{\Env \proves \textsl{string-literal} : \String}{} \\ \\ \RuleSide{\begin{array}{c} \Env \proves e_0 : \Int \quad \Env \proves e_1 : \Int \\ \textsl{op}\in\{\texttt{+,-,/,*,\%}\} \end{array}} {\Env \proves e_0 ~\textsl{op}~ e_1 : \Int }{} & \RuleSide{\Env \proves e_0 : \String & \Env \proves e_1 : \String } {\Env \proves e_0 ~+~ e_1 : \String } {} \\ \\ \RuleSide{\begin{array}{c} \Env \proves e_0 : T_0 \quad \Env \proves e_1 : T_1 \\ T_0 \leq T_1 ~{\textrm or}~ T_1 \leq T_0\\ \textsl{op}\in\{\texttt{==,!=}\} \end{array}} {\Env \proves e_0 ~\textsl{op}~ e_1 : \Bool }{} & \RuleSide{\begin{array}{c} \Env \proves e_0 : \Int \quad \Env \proves e_1 : \Int \\ \textsl{op}\in\{\texttt{<=,<,>=,>}\} \end{array}} {\Env \proves e_0 ~\textsl{op}~ e_1 : \Bool }{} \\ \\ \RuleSide{\begin{array}{c} \Env \proves e_0 : \Bool \quad \Env \proves e_1 : \Bool \\ \textsl{op}\in\{\texttt{\&\&,||}\} \end{array}} {\Env \proves e_0 ~\textsl{op}~ e_1 : \Bool }{} & \RuleSide{\Env \proves e : \Int } {\Env \proves \texttt{-} e : \Int }{} \\ \\ \RuleSide{\Env \proves e_0 : T \arr & \Env \proves e_1 : \Int } {\Env \proves e_0[e_1] : T } {} & \RuleSide{\Env \proves e : \Bool } {\Env \proves \texttt{!}e : \Bool } {} \\ \\ \RuleSide{\Env \proves e : T \arr } {\Env \proves e\,.\,{\tt length} : \Int } {} & \RuleSide{\Env \proves e : \Int} {\Env \proves {\tt new} ~ T [ e ] : T \arr} {} \\ \\ \RuleSide{C {\rm ~is~a~declared~class}} {\Env \proves {\tt new} ~ C\texttt{()} : C} {} & \RuleSide{} {\Env \proves \texttt{null} : \Null} {} \\ \\ \RuleSide{\textsl{id}:T \in \Env} {\Env \proves \textsl{id} : T} {} & \RuleSide{{\tt this}:C \in \Env} {\Env \proves {\tt this} : C} {} \\ \\ \RuleSide{\Env \proves e : C & (\textsl{id}:T) \in C} {\Env \proves e\,.\,\textsl{id} : T} {} \\ \\ \RuleSide{\begin{array}{c} \Env \proves e_0 : T_1 \times \ldots \times T_n \to T_r \\ \Env\proves e_i: T'_i ~~~ T'_i \leq T_i ~~~ \textrm{for all } i = 1..n \end{array} } {\Env \proves e_0(e_1, \ldots, e_n) : T_r}{} & \RuleSide{\begin{array}{c} \t{Library}.m \textrm{ has type } T_1 \times \ldots \times T_n \to T_r \\ \Env\proves e_i: T'_i ~~~ T'_i \leq T_i ~~~ \textrm{for all } i = 1..n \end{array} } {\Env \proves \t{Library}.m(e_1, \ldots, e_n) : T_r}{} \end{array}\]

Typing rules for statements.

\[\begin{array}{c} \RuleSide{\Env \proves e_l : T & \Env \proves e : T' & T' \leq T} {\Env \proves e_l = e } {} \qquad \RuleSide{\Env \proves e : \Bool & \Env \proves S_1 & \Env \proves S_2 } {\Env \proves {\tt if} ~(e) ~{\tt then} ~S_1 ~{\tt else} ~S_2} {} \\ \\ \\ \RuleSide{\Env \proves e : \Bool & \Env \proves S} {\Env \proves {\tt while} ~(e) ~S} {} \qquad \RuleSide{} {\Env \proves {\tt break ~;} } {} \qquad \RuleSide{} {\Env \proves {\tt continue ~;} } {} \\ \\ \\ \RuleSide{\Env \proves e_0(e_1, ..., e_n) : T} {\Env \proves e_0(e_1, ..., e_n) ~{\tt ;} } {} \qquad \RuleSide{\Env \proves e : T ~~~ {\tt ret} : T' \in \Env ~~~ T \leq T' } {\Env \proves {\tt return} ~ e ~{\tt ;} } {} \qquad \RuleSide{{\tt ret} : \Unit \in \Env } {\Env \proves {\tt return ~;} } {} \\ \\ \\ \RuleSide{\Env \proves e : T' & T' \leq T & \Env,\,x:T \proves S } {\Env \proves T ~x = e \,;~ S } {} \qquad \RuleSide{\Env,\,x:T \proves S } {\Env \proves T ~x \,;~ S } {} \qquad \RuleSide{\begin{array}{c} S_1 {\rm ~not ~declaration} \\ \Env \proves S_1 ~~~ \Env \proves S_2 \end{array}} {\Env \proves S_1 ~~ S_2 } {} \end{array}\]

Type rules for class and method declarations.

\[\begin{array}{c} \RuleSide{\begin{array}{c} \textit{classes}(P) = C_1, ..., C_n \\ \proves C_i ~~\textrm{for all } i = 1..n \end{array}} {\proves P} {\text{[PROGRAM]}} \\ \\ \\ \RuleSide{\begin{array}{c} \textit{methods}(C) = m_1, ..., m_k \\ \textit{Env}(C,m_i) \proves m_i ~~\textrm{for all } i = 1..k \\ \end{array}} {\proves C} {\text{[CLASS]}} \\ \\ \\ \RuleSide{\Env, x_1 : t_1, ..., x_n:t_n, {\tt ret}:t_r \proves S_{body}} {\Env \proves t_r ~m(t_1 ~x_1, ..., t_n ~x_n) ~\{~ S_{body} ~\}} {\text{[METHOD]}} \end{array}\]

Here, Env(C,m) is the environment (or scope) for class \(C\) and method \(m\). Thus Env(C,m) contains all of the methods and fields of \(C\), including those declared in \(C\)’s superclasses; it also binds the keyword this to the type \(C\), so that inside any method of class \(C\), the expression this has type \(C\). Finally, classes(C) yields all of the classes in program \(P\); and methods(C) yields the declarations of all methods in the body of class \(C\) (but not those inherited from other classes).

Appendix: Library Functions

IC uses a simple mechanism to support I/O operations, datatype conversions, and other system-level functionality. The signatures of all of these functions are declared as follows:

Library Method Behavior
void println(string s); prints string s followed by a newline
void print(string s); prints string s
void printi(int i); prints integer i
void printb(boolean b); prints boolean b
int readi(); reads one character from the input
string readln(); reads one line from the input
boolean eof(); checks end-of-file on standard input
int stoi(string s, int n); returns the integer that s represents
or n if s is not an integer
string itos(int i); returns a string representation of i
int[] stoa(string s); an array with the ascii codes of chars in s
string atos(int[] a); builds a string from the ascii codes in a
int random(int n); returns a random number between 0 and n-1
int time(); number of milliseconds since program start
void exit(int n); terminates the program with exit code n

To invoke library functions, the program must use method calls qualified with the Library name; for instance, Library.random(100) or Library.stoi(“412”,0). For simplicity, there will be no interfaces for library functions and library calls do not need to be type-checked. For each qualified call Library.f, the compiler will generate a static call to f in the assembly output, regardless of whether f is available in the library or not. Users can extend the standard library libic64.a with additional functions and invoke them using qualified calls in the source program, but one must take care to invoke them with the right number and type of arguments.