Yoix functions can be defined to take a fixed or variable number of arguments
or a combination of the two (i.e., some number of required arguments and
a variable number of subsequent arguments).
The description of a function definition can be summarized as follows:
FunctionDefinition:
name ( ) Compound
name ( ) = Expression
name ( ... ) Compound
name ( ... ) = Expression
name ( ParameterList ) Compound
name ( ParameterList ) = Expression
name ( ParameterList , ... ) Compound
name ( ParameterList , ... ) = Expression
The body of a function, which can be an expression or a compound statement,
always has access to a local
int
variable named
argc
and a local
Array
variable named
argv.
The array starts with a string in
argv[0]
that is the
name
of the function, and ends with the arguments in the same order that they
appeared in the function call.
The value of
argc
is the length of the
argv
array, which means it is one more than the number of arguments that appeared
in the function call.
Note that within a function, the globally defined
argc
and
argv
are hidden by the local definitions, but they always can be accessed by using
global.argc
and
global.argv.
The function definition process is straightforward, but there are
some important points that need to be documented.
A function definition begins with a
name,
which is the string that ends up in
argv[0]
whenever the function is called.
Seems obvious until you realize that once a function is defined it can
be assigned to another variable (or even passed as argument in a function call),
but no matter what happens to that function the original
name
never changes.
Here is a simple example that illustrates the behavior.
Run
Original() {
yoix.stdio.printf("argv[0]=%s\n", argv[0]);
}
Function Fake = Original;
Original();
Fake();
and
argv[0]=Original
argv[0]=Original
prints on standard output.
The second point is much more important.
Defining a function is a runtime activity;
the parser has to agree that a statement is a function definition
and then it has to build a parse tree that represents that definition,
but nothing else happens until the interpreter, at runtime, bumps into
the function definition parse tree.
At that point it builds an internal representation of the function,
hides the function's
name,
the parse tree that represents the body,
and a reference to the current
global
dictionary in the function's internal representation,
and then officially associates
name
with the internal version of the function, which means the function can
finally be used by the Yoix program.
So what happens to the dictionary that is saved with a function?
One of the last things the interpreter does for a function call
is build a block structure to handle variable lookups, and the first step
in that process takes that dictionary and makes it the current
global
dictionary.
The block structure that was in place when the function was called is
not touched, but it is completely hidden because an unsuccessful variable
lookup in Yoix always stops at the current global block.
What happens at that point depends on the
VM.create
flag, but that is a subject we will have to cover elsewhere.
Usually the current global dictionary and the one saved in the function match,
so hiding the current block structure is often the only reason the interpreter
starts a new global scope.
However it is not hard to create a function with its own global dictionary.
Put
String str = "now is the time";
Builder() {
yoix.stdio.printf("global=%O\n", global);
}
return(Builder);
in a file somewhere (e.g., /tmp/xxx), start the Yoix interpreter,
type,
import yoix.stdio.*;
f = execute(fopen("/tmp/xxx", "r"));
f();
and something like,
global=Dictionary[9:0]
Builder=Builder()
VM=Dictionary[22:0]
>argc=1
argv=Array[1:0]
envp=NULL:ARRAY
errordict=Dictionary[7:0]
importdict=Array[0:0]
str=^"now is the time"
typedict=Dictionary[162:0]
prints on standard output.
Look at the output carefully and make sure you understand what just happened.
This is an important example and it should not be hard to imagine how
the technique can be extended, particularly when you realize that
execute
can take additional arguments, the file could be anywhere on the network,
and the function could return something useful, like a frame filled with
other GUI components.
Once the function's
global
scope is ready the interpreter figures out where the function that it
is calling came from, which may not be where it was originally defined.
If the answer is somewhere other than the
global
dictionary, then the interpreter starts a second block
that uses that object (i.e., the one that contains the function) for storage
and as the one that is used to resolve
this;
otherwise
global
and
this
will be synonyms.
The interpreter then prepares another block for the arguments
and finally executes the body of the function.
Much of what we just explained can be illustrated in a few simple examples.
The following program
import yoix.stdio.*;
String str = "Now is";
Dictionary d = {
String str = "the time";
f(String str) {
printf("%s %s %s\n", global.str, this.str, str);
}
};
d.f("for all good men");
prints
Now is the time for all good men
on standard output.
If we pick a new name for the function argument but forget to make any
other changes
import yoix.stdio.*;
String str = "Now is";
Dictionary d = {
String str = "the time";
f(String s) {
printf("%s %s %s\n", global.str, this.str, str);
}
};
d.f("for all good men");
then we get
Now is the time the time
on standard output.
We still have more to explain, but we think this is a decent start,
so the rest of the story will have to wait for another release.
| |
| Example: |
The following is a more complicated example that defines a function
and calls it in different ways.
Variable names intentionally do not clash, which means we could avoid using
global
and
this
in the example.
import yoix.stdio.*;
Function fvbl;
Pointer pfvbl;
String variable1 = "global context";
Dictionary dict1 = {
String variable2 = "dict1 context";
Dictionary dict2 = {
String variable3 = "dict2 context";
func(String info) {
fprintf(stdout, "\nCalling info: %s\n", info);
fprintf(stdout, "variable1: %s\n", variable1);
try {
fprintf(stdout, "variable2: %s\n", variable2);
}
catch(e) {
fprintf(stdout, "variable2 is not available\n");
return(true);
}
try {
fprintf(stdout, "variable3: %s\n", variable3);
}
catch(e) {
fprintf(stdout, "variable3 is not available\n");
return(true);
}
};
fvbl = func;
pfvbl = &func;
};
Function fvbl2 = fvbl;
};
dict1.dict2.func("direct");
fvbl("variable");
(*pfvbl)("pointer");
dict1.fvbl2("dict1 variable");
The output written to standard output would look something like:
Calling info: direct
variable1: global context
variable2 is not available
variable3: dict2 context
Calling info: variable
variable1: global context
variable2 is not available
variable3 is not available
Calling info: pointer
variable1: global context
variable2 is not available
variable3: dict2 context
Calling info: dict1 variable
variable1: global context
variable2: dict1 context
variable3 is not available
| | |
| See Also: |
Function,
global,
reference,
this,
unroll
|
|