Parameter Passing

From Rosetta Code

Parameters of a subprogram refer to its arguments and results. A parameter can be passed in one of two modes:

  • by value (or else by copy)
  • by reference

When the parameter is passed by value, within the subprogram the object denoted by the formal parameter (the identifier of the parameter) is distinct from the object of the actual parameter (the value that has been passed in). In particular, the formal parameter has an independent set of states, so that if the object is mutable, then updates to the object do not influence the state of the actual parameter object, so long as the program has not left the subprogram.

When the parameter is passed by reference, the formal object represents an aliased view of the actual object. Thus, updates of the formal object are instantly reflected by the actual object.

The mutability of the parameter is independent on the parameter passing mode. In general the formal parameter can have any of three access modes:

  • in (immutable)
  • out (update only, usually used for the results)
  • in-out (mutable)

For example, when an in-out parameter is passed by value, the compiler creates a copy of it and makes the copy available to the subprogram. After the subprogram completion, the value of the copy is written back to the actual object.

The language's choices of parameter passing modes may vary greatly, as the following examples show.

Example 6502 Assembly

As is typical of assembly languages, 6502 Assembly doesn't restrict how parameters are passed to functions in any way. Read-only data is of course immutable, but this is just the nature of read-only memory and not a limitation imposed by the language.

Opcodes

Certain commands such as INC,DEC,ASL,LSR,ROL, and ROR inherently alter the memory address that is used as an argument. Thus their use on a memory address is pass-by-reference.

ROL $81 ;rotate left the bits of the value stored at memory address $0081. The old value of $0081 is discarded.
INC $20 ;add 1 to the value at memory address $20.

However, opcodes that use a register as their primary operand are strictly pass-by-value.

ADC $81   ;add the value stored at memory address $81 to the accumulator. The value at memory address $81 is unchanged. 
          ;If the carry was set prior to this operation, also add an additional 1 to the accumulator.

LDX $20   ;load the value at memory address $20 into the X register.
DEX       ;subtract 1 from the value in the X register. This is only a copy. The actual value stored at $20 is unchanged.
          ;Keep in mind that this is true for INX/INY/DEX/DEY but not INC/DEC.

BIT $2002 ;Set the flags as if the value stored at address $2002 was bitwise ANDed with the accumulator. 
          ;Neither the accumulator nor the value stored at $2002 is changed.

LDA $40   ;load the value stored at memory address $40 into the accumulator
ROR A     ;rotate right the accumulator. This is only a copy. The actual value stored at $40 is unchanged.

The main takeaway from this is that by default, the use of LDA/LDX/LDY commands to load from memory is pass-by-value. In order to actually update the memory location, you must store the value back into the source memory address.

Functions

Whether a function's parameters are pass-by-value or pass-by-reference ultimately depends on how it is written and which opcodes are used. Generally, parameters are either passed using the stack or with temporary zero-page memory addresses. While these are both forms of memory that are getting overwritten, the actual sources of the data remain the same.

Example of in (immutable)
    lda sourceData
    jsr MultiplyBy4


MultiplyBy4: ; pretend this routine is somewhere far away.
    lsr A
    lsr A
    rts
Example of out (result)

This routine reads the joystick output on Commodore 64 and stores it in the output variable joystick1.

ReadJoystick:
    lda $DC00      
    ora #%11100000
    sta joystick1
    rts
Example of in/out (mutable)

This one's tricky because of the way pointers work on the 6502. Unlike the other architectures of its day there are no "address registers" per se, but we can make our own using the zero page.

    LDA #$03 ;load the low byte of the parameter's address
    STA $20  ;store it in the low byte of our pointer variable.
    LDA #$00 ;load the high byte.
    STA $21  ;store it in the high byte of our pointer variable. 

    ;we have to do it this way because a simple "LDA $03" cannot remember the source address after it is executed.
    ;now we can LDA ($20),y and STA ($20),y to load and store to/from the same place, regardless of where that place actually is.

    LDY #0          ;6502 requires Y to get involved, set it to 0 to have no offset.
    jsr Add32


Add32:              ; pretend this subroutine is somewhere far away.
    LDA ($20),y     ; the input parameter is actually address $0003
    clc
    adc #$20        ; 0x20 = decimal 32
    sta ($20),y     ; overwrite the old value stored in $0003 with the new one.
    rts

Example 68000 Assembly

68000 Assembly is very similar to 6502 Assembly in this regard. Parameters are typically passed via the stack or through registers. For human-written assembly, the programmer can pass an argument by reference by passing a pointer to the argument rather than the argument itself. Of course, the function will need to dereference that pointer; however whether the function writes back the new value to that memory address is also decided by the programmer. This means that just because a parameter is passed by reference does not mean that it gets altered by the function that received it. This (contrived) example shows this concept in action.

start:
MOVE.L #$00FF0000,D0 ;load D0 with the pointer 0x00FF0000 (I decided this was a pointer just for example's sake.)
MOVE.L D0,-(SP)      ;push that value onto the stack.
MOVE.L #$12345678,D0 ;load the constant 0x12345678 into D0
MOVE.L D0,-(SP)      ;push that value onto the stack.
JSR foo

;somewhere else far away from start
foo:
MOVE.L (4,SP),D0  ;load the pushed parameter 0x12345678 into D0
MOVE.L (8,SP),A0  ;load the pushed parameter 0x00FF0000 into A0
MOVE.L (A0),D1    ;dereference the pointer (we're treating it as a pointer to an int)
ADD.L D1,D0       ;add the values.
RTS               ;and return

Example Ada

Ada uses both by value and by reference passing and all three parameter access modes:

  • objects of the scalar types are passed by value
  • objects with an identity are passed by reference. Objects with identities are task types, protected types, non-copyable (limited) types, tagged types (OO objects with type identity)
  • for all other types the choice is left to the compiler.

Example ARM Assembly

Whether an assembly language uses pass-by-value or pass-by-reference mostly depends on the compiler and the language the source code was written in, if not written directly in ARM Assembly.

Early versions of the ARM were unable to directly operate on memory without loading the value into a register first. So almost all commands were inherently pass-by-value unless they explicitly stored an updated value in the same memory location.

;this example is for the ARM7TDMI and might not be true for newer revisions.
mov r0,#memoryAddress          ;r0 contains the memory address, not the value stored within.
mov r1,#anotherMemoryAddress   ;r1 contains the memory address, not the value stored within.
add [r0],[r0],[r1] ;won't work, you have to LDR the value into a register, work on it there, then store it back.

ldr r0,memoryAddress           ;load the VALUE stored at "memoryAddress" into r0
ldr r1,anotherMemoryAddress    ;load the VALUE stored at "anotherMemoryAddress" into r1
mov r2,#memoryAddress          ;load the MEMORY ADDRESS into r2
add r0,r0,r1                   ;add r1 to r0 and store the result in r0
str r0,[r2]                    ;store the value of r0 into the MEMORY ADDRESS contained in r2.

Typically when talking about pass-by-value or pass-by-reference, a programmer is referring to the contents of a memory address. The stack and the CPU's data registers are not included in the conversation. It's expected that a data register gets "clobbered," after all, it's impossible to do actual math otherwise. Or so you might think, but the ARM takes the concept of "pass-by-value" one step further. The data registers themselves can be operated on in some ways without altering their contents:

  • The data register where a math operation is stored can be different from both operands.
  • In any operation, the contents of a data register can be bit-shifted or bit-rotated just for that operation, and the actual value is unchanged.

x86 Assembly can do neither of the above; at least one of its registers used as an operand must be the destination for the result.

Example C/C++

C and C++ use only by value parameter passing. Arguments are mutable as if they were in-out, but the compiler does not store the copies back. Results also passed by copy (copy out). Where by-reference passing is needed, the C/C++ deploys referential types T* and T& (in C++). So when an object of the type T needs to be passed by reference, another object of either the type T* or T& is passed by value instead. Because objects in C++ can be automatically converted to references, in many cases the difference is not noticeable.

Example Fortran

Early versions of the language used only by reference parameter passing for the arguments, but the formal parameters were copied locally and then copied back to the actual parameters at the end of the call. This is also known as "call by value result" or "call by copy-restore".

Starting from Fortran 90 (and later), functions and subroutines can specify an intent for the argument; intent can be: in (unmodifiable argument), out (argument used to return a value; the starting value must be intended not significant, undefined), inout (argument both is intended for input and can be modified, i.e. return the value modified by the function/subroutine).

Trying to modify an argument with intent in triggers an error at compile time.

Example M2000 Interpreter

All parameters are passed by value is a stack of values. This stack is a collection of values, and that collection is passed from a module to a module as is, and also used for return back values. Functions start with a fresh stack of values, and at that stack all the parameters from the call stored there. To read parameters from start of values we use Read. A definition Function a(k as integer) {code here} is identical with that: Function a {read j as integer : code here}. Because we can use read at any point of a module or function we can handle parameters with many ways before actually pop them from the stack, like we can check type, we can rearrange and use the stack of values for any use.

We can pass by reference values, although actually we store a weak reference and at the Read statement this reference change to normal if it is valid (or an error occur). So call thisModule &Z pass a string as a weak reference, and at the thisModule we have the Read &t which make t to hold the same memory as the Z.

Some objects are passed by value but because we pass the pointer to object we can change it. If we pass an array with parenthesis like thisModule A() we pass a pointer but the interpreter knows that this is a by value pass so at read statement the Read K() copy the A() to K(). If we wish to pass by reference then we use this thisModule &A() but the Read statement has to be prepared to get the reference like this Read &K(). So we can't use the same parameter by value and by reference at will, we have to make the code at read statement to be by value or by reference. We can use code to examine the type of parameter in the stack of values and then decide which read statement to use. So there is always a way to program a stack handler, to cope with by parameter types, although there is no automatic except for those which is always by reference and those which is always by value.

We can pass by reference functions. This is a copy of function code (as string) inside a block {} so the Read statement if we setup like this Read &A() expect to find a string which have a block of code {}.

A reference can't get as second reference. But we can use temporary block using For This { } so anything we make there just deleted after exit from block, so the next time we enter the block we may define a new reference (because the name which take the reference is new). Some times we use a special call, the "call local" which place the current scope to a module or function (which its called as module), so there we use Read New to make any local name as a new name, including by reference passing values (to shadow same names from same scope module). This used for event service functions.

This show how we can make a function reference (just a copy of code), and a second function which use the same scope. We can use lazy$() which make this automatic for a function k() as lazy$(&k()) which put the module and the module name to code of k(). Here we see only the way we do this simply by using strings. Module$ return the internal compact name of module. Use module.name$ to print the expanded named.

module kappa {
	push "{read x, y:=x**y}"
	read &a()
	print a(2, 3)=8
	long m
	push "{module "+module$+" : m++}"
	read &b()
	call b()
	print m=1
}
call kappa

Array items is a special case. Because arrays are objects which hold the actual array, we can pass an array item by reference and interpreter use a copy in/copy out method, so we actually get the value of array item in a local value (the parameter) and at the exit from call, interpreter feed the array back (if the array item exist, because we can pass the array and deallocate items, reducing the length of array). We can't pass by reference array items to subs and simple functions (these two structures are light versions, which didn't have logic to do something for this).

module checkArr (m){
	dim arr(10) as long=10
	module kappa (&x as long, &a(), m) {
		dim a(m)
		x++
	}
	kappa &arr(3), &arr(), m
	try ok {
		print arr(3)=11
	}
	if not ok then print "nothing to show"
}
checkArr 2  ' nothing to show
checkArr 5 
Module checkArr (m){
	function kappa(&x as long, &a(), m) {
		dim a(m)
		x++
	}
	dim arr(10) as long=10
	kk=kappa(&arr(3), &arr(), m)   // this call use new stack of values.
//	call kappa(&arr(3), &arr(), m)  // you can use call kappa() also, passing the current stack of values, like module's call.
	try ok {
		print arr(3)=11
	}
	if not ok then print "nothing to show"
}
checkArr 2
checkArr 5

Example Z80 Assembly

Like with other assembly languages, how parameters are passed is up to the programmer, and the CPU doesn't restrict how it's done. Read-only memory is immutable of course. Typically, parameters are passed in by registers or the stack. Registers are quicker and don't require you to "dance around" the return address of a function call, but the stack is much larger than the Z80's pool of registers ever could be.

The key to passing by value or by reference when needed is to know which operations directly affect memory and which do not. The following instructions directly alter memory (this is not an exhaustive list):

  • EX (SP),HL
  • INC (HL), INC (IX+##), INC (IY+##)
  • DEC (HL), DEC (IX+##), DEC (IY+##)
  • RES #,(HL), RES #,(IX+##), RES #,(IY+##)
  • SET #,(HL), SET #,(IX+##), SET #,(IY+##)
  • RL(HL), RL(IX+##), RL(IY+##)
  • RR(HL), RR(IX+##), RR(IY+##)
  • RLC(HL), RLC(IX+##), RLC(IY+##)
  • RRC(HL), RRC(IX+##), RRC(IY+##)
  • SLA(HL), SLA(IX+##), SLA(IY+##)
  • SLL(HL), SLL(IX+##), SLL(IY+##)
  • SRL(HL), SRL(IX+##), SRL(IY+##)
  • SRA(HL), SRA(IX+##), SRA(IY+##)
  • LDI, LDD, LDIR, LDDR