The syntax of a language must be structured enough that it can be automatically recognized by an implementation. This requires describing:
A language implementation provides a couple of very standard capabilities:
/* Can you guess what this function is doing? */
int factorial(int x, int sum) {
return x + sum * previous;
}
/* How about with these Names? */
int next_height(int height, int velocity) {
return height + velocity * ELAPSED_TIME;
}
+---------------------+----global name space
|int x; |
| |
|main (...) { |
| +----------------+------main name space
| |int x; | |
| | { | |
| | +-----------+--------block inside main name space
| | |int x; | | |
| | | | | |
| | +-----------+ | |
| | } | |
| | ... | |
| +----------------+ |
| } |
| |
|foo (...) { |
| +----------------+------foo name space
| |int x; | |
| | ... | |
| +----------------+ |
| } |
+---------------------+
{--- Pascal example}
procedure cat (...);
var dog : Integer; {--- override any external binding of "dog"}
begin {--- start of the block}
...
end; {--- end of the block, "dog" returns to external binding (if any)}
A variable in a program block can be thought of as free if it is not bound within the block (e.g., an access to a global variable from within a function is an access to a free variable). Consider the following C program fragment.
int x = 3;
foo(int y) {
x = y; /* x is free, y is bound */
}
It is important to identify free vs. bound variables since free variables can have different bindings/meanings each time the block is evaluated (the binding depends on something external to the function).
int X;
void foo (int X)
{
...
}
int main() {
int X;
foo(X);
...
}
Here the interpretation of X depends on the current bindings of X. X can refer to either a global variable, a variable local to main or a parameter of the function foo.
int X;
void foo (int X)
{
(3)
...
}
int main() {
(1)
int X;
(2)
foo(X);
...
}
The environment at point (1) is:
X -> a global integer variable foo -> a function abstraction
At point (2):
X -> an integer variable local to main foo -> a function abstraction
At point (3):
X -> a parameter of the function foo foo -> a function abstraction
There are two obvious interpretations of bindings, called static bindings and dynamic bindings. Static binding (or static scoping) involves the function body being evaluated in the environment of the function definition also referred to as the lexically enclosing environment. This is what we have seen in the examples so far. Dynamic binding (or dynamic scoping) is slightly different in that the function body is evaluated in the environment of the function call.
For example, consider the following Pascal code:
const s = 2;
const h = 10;
function scaled (d : Integer);
begin
scaled := d * s;
end;
function call_scaled(blah : Integer);
const s = 3;
begin
... scaled(h);
end;
begin {---main}
...
end.
If the above program uses static binding, s is evaluated at the point of the function definition (i.e. in the function scaled) and the value of scaled(h) is 20. If dynamic binding is used, s is evaluated at the point of the function call (i.e. in the function call_scaled) and the value of scaled(h) is 30.
With static binding, we can determine the bindings of identifiers at compile time, but with dynamic binding we must delay this until run-time. As such, most programming languages use static binding.
int x; /* storage for global variables */
void main() {
int y; /* stack storage for local variables */
char *str; /* stack storage for local variables */
str = malloc(100); /* allocates 100 bytes of dynamic heap storage */
y = foo(23);
free(str); /* deallocates 100 bytes of dynamic heap storage */
} /* y and str deallocated and stack frame popped */
int foo(int z) { /* z is allocated stack storage */
char ch[100]; /* ch is allocated stack storage */
if (z == 23)
foo(7);
return 3; /* z and ch are deallocated as stack frame is
popped, 3 is put on top of the stack */
}
/* C example */
char *str;
str = malloc(100); /* allocate a 100 byte storage object in heap storage
and put the pointer to it into str */
free(str); /* explicitly release the allocated storage object */
/* C example */
char *str;
str = malloc(100); /* allocate a 100 byte storage object in heap storage
and put the pointer to it into str */
free(str); /* explicitly release the allocated storage object */
*str = 'h'; /* str is now a dangling reference! This instruction
attempts to put the character 'h' into what is
pointed at by str, but str points to a storage
that is no longer allocated to the program */
A value of a composite type consists of components that may be inspected selectively, and these components in turn may have components that may be inspected selectively.
Features:
# Perl
(3, 4, 5) # creates a list with three elements
# one element in each
/* C example */ int int_array[] = (1,2,3,4,5); /* int_array has 5 items - 1,2,3,4,5 */ int_array[2] = 6; /* int_array now contains 1,2,6,4,5 */
/* Ada example */ type Real_array is array [1..10] of Real; var a : Real_array; a[1] := 3; a[11] := 4; /* error - out of bounds! */
/* Ada example */
procedure bar(m : integer) {
type Real_Array is array (Integer range <1..10>) of Real;
a : Real_Array (2..m); /* uses the current value of m */
a[4] := 3; /* maybe out of bounds! */
a[11] := 4; /* error - out of bounds! */
The advantage of this is that the array size can be set ``on the fly'', but it is still subject to a maximum size.
// the mighty Java
Vector any_array = new Vector(); // initially size 10 (by default)
any_array.addElement(1);
any_array.addElement(2);
...
any_array.addElement(10);
any_array.addElement(11); // out of bounds - Java doubles the size of
// the array (doubles by default)
Perl also has flexible arrays.
A[i] = V.O. + i x E.S.
/* C example */
void swap(int *x, int *y) {
...
}
...
int a,b;
swap(a, b); /* type mismatch error! */
# Perl example $x = "Hello World\n"; # x has a value of type string $x = 3; # x has a value of type integer $z = "5"; # z has a value of type string $x = $z + $x; # convert z into an integer and add
typedef int new_int; int z; new_int y; z = y; /* not name equivalent, so fails type check! */
typedef int new_int;
typedef struct {
int value;
} simple_type;
typedef struct {
int value;
} another_type;
typedef struct {
int value;
char kind;
} a_third_type;
int z;
new_int x;
simple_type var_one;
another_type var_two;
a_third_type var_three;
z=x; /* OK by structure, not by name */
var_one = var_two; /* OK by structure, not by name */
var_one = var_three; /* not the same structure */
The program:
PROGRAM MAIN(INPUT, OUTPUT) ;
PROCEDURE P(V: INTEGER) ;
VAR
X: INTEGER ;
PROCEDURE Q(V: INTEGER) ;
BEGIN
X := V ;
WRITELN("In Q -- X = ", X:2, ", V = ", V:2) ;
END ;
PROCEDURE R(V: INTEGER) ;
VAR
X: INTEGER ;
PROCEDURE S(V: INTEGER) ;
BEGIN
X := V ;
Q(V+1) ;
WRITELN("In S -- X = ", X:2, ", V = ", V:2) ;
END ;
BEGIN
X := V ;
S(V+1) ;
WRITELN("In R -- X = ", X:2, ", V = ", V:2) ;
END ;
BEGIN
X := V ;
R(V+1) ;
WRITELN("In P -- X = ", X:2, ", V = ", V:2);
IF (V < 45000) THEN
P(48048) ;
END ;
BEGIN
P(43680) ;
END .
The output of this Program:
In Q -- X = 43683, V = 43683 In S -- X = 43682, V = 43682 In R -- X = 43682, V = 43681 In P -- X = 43683, V = 43680 In Q -- X = 48051, V = 48051 In S -- X = 48050, V = 48050 In R -- X = 48050, V = 48049 In P -- X = 48051, V = 48048