Programming Languages & Compilers

3,798 questions on Programming Languages & Compilers, part of Computing & Information Sciences. Below are 12 of them in full, each answered in plain language.

Questions & explanations

1. Compare Scott topology with the Alexandrov topology on a poset.

The Alexandrov topology on a poset has all upper sets as open. The Scott topology is a subset of the Alexandrov topology: only those upper sets that are also inaccessible by directed joins. So every Scott-open set is Alexandrov-open, but not vice versa. For example, in the domain of natural numbers with bottom, the set of all numbers greater than 0 is Alexandrov-open but not Scott-open because the directed set of all finite numbers has join ∞ (which is not an element), but the condition requires being inaccessible to directed joins of existing points. Actually, careful: in that domain, the set {1,2,3,...} is upper set but not Scott-open because the directed set {1,2,3,...} has least upper bound ∞ (not in domain). But the condition for Scott-open is: if a directed set has join in the open set, then some element of the directed set is already in the open set. For the set of all positive numbers, consider the directed set of all numbers {⊥,1,2,3,...}. Its join is not in the domain? Actually, in a complete domain, every directed set has a least upper bound. The set {1,2,3,...} is directe

2. Give an example of a term that is typable in System F but not in simply typed lambda calculus.

The term λx. x x is not typable in simply typed lambda calculus because it would require a type like τ → σ for x such that x can be applied to itself. In System F, we can assign type ∀α. (α→α) → α→α? Actually, let Λα. λx:α→α. x x is not valid because x x expects an argument of type α, but x has type α→α, so x x would require x of type (α→α)→ something. More standard example: the term Λα. λx:α. x has type ∀α.α→α. But that is also not typable in simply typed? Actually simply typed can have λx:α.x only if α is a base type? No, simply typed does not have type variables. So any term using a type abstraction is not typable. A concrete example: the self-application term λx.x x can be typed in System F with a recursive type? Actually, in System F, (λx. x x) (λy. y) is not typable because the function part is not polymorphic enough. The standard example is the encoding of natural numbers: Church numerals are typable in System F but not in simply typed because they require impredicative polymorphism. So a Church numeral like λf.λx. x is typable in System F as ∀α.(α→α)→α→α.

3. Give an example of a system from the lambda cube that has type-dependent types but not kind-dependent types.

System Fω is at the corner of the cube with both polymorphism and type operators, but it does not have dependent types. However, the question asks for type-dependent types (likely meaning types depending on types, i.e., type operators) but not kind-dependent types (higher-kinded types). Actually, the cube axes: (1) terms depending on types = polymorphism, (2) types depending on types = type operators, (3) types depending on terms = dependent types. So a system with type operators but no polymorphism? That would be λω. But λω typically does have polymorphism? Actually, the cube has eight systems. λ_→ (simply typed) has none. λ2 (System F) has polymorphism only. λω has type operators only? Actually, the cube's axes are independent. λω has types depending on types (operators) but not polymorphism nor dependent types. So λω is an example. For clarity, λω allows type-level functions, e.g., λτ:∗. τ→τ, but no ∀ over types. So answer: λω (called system Fω without polymorphism) has type-level functions but not type abstraction.

4. Explain contravariance with an example like function arguments.

Function arguments are contravariant. If you have a function that takes an Animal, you can pass a function that takes a Dog instead? Actually, the opposite: If a function expects a function that takes Animal, you can provide a function that takes a Mammal (which is broader, not narrower). Contravariance means if A is subtype of B, then function from B is subtype of function from A. For example, a function that can handle any Animal can be used where a function that only handles Dogs is expected? That is tricky. Better: A function taking an Object is a subtype of a function taking a String? Actually, the common example: The Comparable interface is contravariant in its type parameter. In Java, ? super T makes it contravariant.

5. Give an example of nested loops where interchanging them improves cache performance.

Consider a matrix multiplication code that has outer loop over i (rows) and inner loop over j (columns). Accessing A[i][k] in the inner loop is fine, but if the inner loop is over k (the middle index), then B[k][j] is accessed sequentially only if j is innermost. Typically, for row-major, loops should be ordered so the last index varies fastest. For two nested loops over i and j, if the array is stored row-major, the inner loop should be j. Interchanging from (i,j) to (j,i) makes accesses sequential if the array is accessed in column order, but careful: in row-major, keep innermost as j. A common example is a cache-friendly matrix multiply using loop interchange to make innermost loop over k for A and over j for B.

6. Compare how a compiler schedules instructions for a RISC pipeline vs a CISC with variable-length instructions.

RISC processors have fixed-length instructions and a simple pipeline with few stages. The compiler schedules instructions to avoid stalls by arranging independent instructions after a load. For variable-length CISC (Complex Instruction Set Computer) instructions, the pipeline is more complex because instructions take different numbers of cycles. The scheduler must handle changes in instruction length, which affect fetch and decode stages. CISC backends often use a different approach, like reordering micro-operations instead of whole instructions. They also need to handle complex addressing modes that may take extra cycles. So RISC scheduling is simpler and more straightforward than CISC scheduling.

7. What are source-to-source transformations?

Source-to-source transformations are changes made to a program's source code before it reaches the compiler's backend. They rewrite the code into another high-level form that is easier or faster to run. These changes aim to improve performance aspects like speed, memory use, or energy consumption. They always preserve the program's exact meaning and behavior. Common examples include loop unrolling, which repeats loop bodies to reduce overhead, and function inlining, which replaces function calls with the function's code. Unlike low-level optimizations that work on assembly, these transformations stay at the source language level. This keeps them readable and easier for programmers to verify.

8. Compare loop-invariant code motion with common subexpression elimination.

Both optimizations avoid redundant computations but target different situations. Loop-invariant code motion moves an expression that is constant throughout the loop to before the loop, so it is computed once instead of many times. Common subexpression elimination finds expressions that compute the same value in different places (not necessarily loops) and reuses the earlier result. For example, if the same expression appears in two separate blocks, CSE replaces the second with a reference to the first. Both reduce the number of arithmetic operations. Loop-invariant code motion is specific to loops, while CSE works across basic blocks. They can be applied together for better performance.

9. Compare tree pattern matching with table-driven code generation (e.g., using a template or rule-based approach).

Tree pattern matching is a specific type of rule-based approach where rules are patterns that match tree structures. Table-driven code generation often uses a set of templates that are matched sequentially, which can be simpler but may not find optimal sequences. Tree pattern matching with dynamic programming guarantees an optimal covering given the patterns. Table-driven approaches may be faster but less flexible. Tree pattern matching is more systematic and easier to retarget to different architectures by providing new pattern sets. Both are used in real compilers; for example, LLVM uses a variant of tree pattern matching (via TableGen) to generate instruction selectors.

10. Give a scenario where linear scan register allocation might perform worse than graph coloring.

Linear scan may produce more spills when live intervals have complex shapes, such as when a variable is live in multiple disjoint ranges (hole in its interval). Graph coloring can handle such cases better because it considers the full interference pattern. Also, linear scan's greedy spilling heuristic can make suboptimal choices, e.g., spilling a short-lived variable that would fit later, leading to extra loads and stores. For loops with high register pressure, graph coloring can often keep important variables in registers across iterations, while linear scan might spill them. However, the performance gap can be narrowed with improvements like second-chance binpacking.

11. What makes a compiler backend for a VLIW processor different from one for a superscalar RISC?

A VLIW (Very Long Instruction Word) processor expects the compiler to schedule instructions in a fixed wide instruction word. The compiler must pack multiple operations into one word and decide which operations can run at the same time. RISC processors can reorder instructions at runtime using hardware, so the compiler has less scheduling work. For VLIW, the compiler needs to know the exact number of functional units and their delay. This makes the VLIW backend more complex and dependent on the hardware details. In contrast, a RISC backend focuses on filling delay slots and avoiding hazards. Overall, VLIW backends shift scheduling effort from hardware to software.

12. How is a source-to-source transformation different from a machine-level optimization?

A source-to-source transformation changes the program while it is still in a high-level language like C or Java. A machine-level optimization works on the final assembly or machine code. Source-level transformations keep the code readable and portable across different hardware. Machine-level optimizations often use specific details of the processor, like register names or instruction schedules. The main advantage of source-to-source is that it is easier for programmers to trust and debug. Machine-level optimizations can achieve more fine-grained speedups but are harder to understand. Both aim to improve performance while keeping the program's behavior unchanged.

More Computing & Information Sciences topics

This page shows 12 of 3,798 questions on this topic. The full set, with progress tracking and five agent perspectives per question, is in the JupiteX app — browse the exam catalogue or browse the Learn library.