Today's Reading: Crafting Programming Languages (2024-01-28)

LLMs are Great for Teachers Too (Not Only Students)

In evening role as an amateur teacher of competitive programming and computer science I aspire to providing thorough and concise lessons (to my daughters).

We were exploring (at a high level) the differences between various programming languages. I must admit I struggled to explain how passing of arrays works in C vs Golang vs Python.

Here is how tremendously useful ChatGPT 4 was:


prompt: What are the differences of what happens with the stack when an array of integers is passed to a function in C versus Golang versus Python?


ChatGPT 4 answered:

When an array of integers is passed to a function, the behavior at the stack level can vary significantly between programming languages like C, Go (Golang), and Python. Here’s a general overview of what happens in each language:

C

  1. Fixed-Size Arrays: In C, arrays are essentially pointers to the first element of a block of memory. When an array is passed to a function, what is actually passed is a pointer to the first element of the array. The array itself is not copied.
  2. Stack Behavior: The pointer (address of the first element of the array) is pushed onto the stack. The actual array elements are not copied onto the stack; they reside in the same memory location where they were originally defined (could be stack memory if defined locally).
  3. Size Information: C doesn’t inherently know the size of the array once it’s passed to a function. You often need to pass the size of the array as an additional argument.

Golang

  1. Slices: Go uses slices more commonly than arrays. Slices are descriptors containing a pointer to the array, the length of the segment, and its capacity. When you pass a slice to a function, you’re passing this descriptor, not the actual array.
  2. Stack Behavior: The slice descriptor (pointer, length, and capacity) is copied onto the stack. The underlying array is not copied, similar to C.
  3. Arrays: If you use an actual array (not a slice) in Go, and pass it to a function, the entire array is copied unless you explicitly pass a pointer to it.

Python

  1. Dynamic Arrays (Lists): In Python, what we typically use as an array is a dynamic array, known as a list. Lists are objects that contain references to their elements and are passed by reference.
  2. Stack Behavior: When a list is passed to a function in Python, a reference (pointer) to the list object is pushed onto the stack. The list itself is not copied. Any modifications to the list within the function are reflected in the original list.
  3. Immutability: Unlike C and Go, Python’s integers are immutable. So, even though the list is passed by reference, modifying its content with new integers means placing new objects in the list, not altering the integers themselves.

Summary

  • C: Passes a pointer to the first element of the array, size often needs to be passed separately.
  • Go: For slices, passes a descriptor; for actual arrays, copies the entire array unless passed by pointer.
  • Python: Passes a reference to the list object; modifications to the list inside the function reflect in the original list.

This high-level overview captures the general behavior in each language. It’s important to remember that the specific behavior can depend on additional factors, such as whether optimizations are applied by the compiler/interpreter, and the exact way arrays or array-like structures are used in your code.


To Illustrate:

C

#include <iostream>
using namespace std;
void mutate(int arr[3]) { arr[1]+=1; }
void print(int arr[3]) {
    cout << "arr: ";
    for(int i=0; i<3; i++) cout << arr[i] << " ";
    cout << endl;
}
int main() {
    int arr[3] = {1,2,3};
    cout << "before mutation - "; print(arr);
    mutate(arr);
    cout << "after  mutation - "; print(arr);
}
$ g++ arrays.cc -o arrays && ./arrays

before mutation - arr: 1 2 3 
after  mutation - arr: 1 3 3

Go

package main

import "fmt"

func mutateSlice(sl []int) {
        sl[1] += 1
}

func mutateArray(ar [3]int) {
        ar[1] += 1
}

func main() {
        var slice = []int{1, 2, 3}
        var array [3]int = [3]int{1, 2, 3}
        fmt.Printf("slice before mutation - %+v\n", slice)
        mutateSlice(slice)
        fmt.Printf("slice after  mutation - %+v << diff\n", slice)

        fmt.Printf("array before mutation - %+v\n", array)
        mutateArray(array)
        fmt.Printf("array after  mutation - %+v << same\n", array)
}

$ go run ./arrays.go

slice before mutation - [1 2 3]
slice after  mutation - [1 3 3] << diff
array before mutation - [1 2 3]
array after  mutation - [1 2 3] << same

Python

#!/usr/bin/env python3
def mutate(lst):
    lst[1] += 1
l = [1, 2, 3]
print("list before mutation - ", l)
mutate(l)
print("list after  mutation - ", l)
$ ./arrays.py

list before mutation -  [1, 2, 3]
list after  mutation -  [1, 3, 3]

And Now for Something Completely Different

  • Did you know that there is at least one occurrence of the string MSDOS in OpenBSD’s source code? Here it is!
    • Line 1345 of /lib/libz/ChangeLog says:
  - Use default memcpy for Symantec MSDOS compiler