the compiler substitutes the line where the functions are called for the statements in the function body
That is something a compiler can optionally perform, known as "procedural integration", or more commonly "inlining".
Inlining is not the normal way of implementing procedure calls. Firstly, inlining all calls would cause compiled programs to explode in size, and potentially to run slower. (Inlining can speed up programs, but only when judiciously applied: an example of where it may help is when a small function that is executed many times in a loop is hoisted into that loop.)
Secondly, calls can be recursive. Recursion cannot be implemented by inlining; it is logically impossible for a function to be inserted into itself: this leads to infinite regress.*
The bare essence of a procedure call is that the calling procedure is suspended, while the called procedure executes. When the called procedure finishes executing, the caller is resumed. This semantics is preserved under inlining; it still looks like the surrounding procedure is suspended while the inlined one executes.
A regular non-inlined procedure call compiles into some kind of control transfer, whereby the machine, instead of executing the next instruction, is told to "jump" to some other list of instructions, corresponding to the body of the procedure. The previous location is somehow remembered. When the called procedure finishes, it "jumps" back to the remembered location, so the calling procedure resumes from that point. When multiple places in the program call a non-inlined procedure, they all share the same image of that procedure: the same list of instructions.
Both procedures and pure functions are susceptible inlining optimizations; there is no difference in that regard. The semantics is preserved; the inlined call appears to be passing parameters and returning a value.
The terminology like procedure and function varies from programming language to programming language.
How the words are used in computer science is that a function corresponds to the mathematical concept of a pure mapping from one abstract space to another. A function returns a value that is calculated from some arguments, which is done without side effects.
A procedure is a name given to some steps which perform some effects, making changes to the state of the machine. Those steps are invoked by their name. Procedures may look like functions in that they have arguments, and return values.
Some languages, like C, call everything function: printf
is called a "function", even though it has a side effect of performing output. exit
and abort
are "functions", though they destroy the process and do not return.
When procedures are called functions, you may hear the term "pure function"
in reference to functions that don't have side effects and only calculate a value.
Common Lisp uses the function terminology similarly to C; both functions and procedures are defined using defun
and are called functions. Scheme uses the term "procedure". So even related language dialects don't necessarily agree.
You have to keep the concepts clearly separated from the way your programming language documentation refers to them. Don't assume that because your language reference manual says "function", that it's talking about a pure mapping between sets.
(There are subtleties in the "function" concept because in computer science, which is after all a branch of mathematics, "function" can refer to a mathematical function, or to a computational function which calculates some mathematical function using concrete algorithmic steps. The two are not the same because not all mathematical functions are computable by a corresponding computational function. For instance the "halting function" exists in mathematics, but cannot be computed. The halting function H has two arguments: a program P and an input I. It maps these to a Boolean value: true
if the program P halts when given input I, or false
if it doesn't halt. The mathematical function is real because every P(I) pair either halts or doesn't halt. But we cannot implement such a function.)
* Of course, partial inlining of recursion is possible. For instance, in the simple case of a function calling itself, a compiler could decide to inline some of the self-calls to some specific depth. The inlining will "bottom out", so that some of the calls remain non-inlined.