LMU ☀️ CMSI 585
PROGRAMMING LANGUAGE FOUNDATIONS
HOMEWORK #5 Due: 2024-06-21

Learning Objectives

With this assignment you will demonstrate:

Readings and Videos

Please:

Instructions

Submit, to BrightSpace, a neatly typeset answer to Problems 1 and 3, and a link to a GitHub repo or Replit for Problem 2. Please make a best effort for typesetting. Handwritten answers are okay if you are short on time, but pay extra attention to neatness if you go that route.

Problems to Submit

  1. Give a Denotational Semantics for a small extension to Bella with arrays, booleans, and lists, that is strongly-typed and dynamically typed. Here is the abstract syntax:

    $ \begin{array}{l} n\!: \mathsf{Numeral} \\ i\!: \mathsf{Identifier} \\ e\!: \mathsf{Expression} = n \;|\; i \;|\; \mathtt{true} \;|\; \mathtt{false} \;|\; \mathit{uop} \; e \;|\; e_1 \; \mathit{bop} \; e_2 \;|\; i \; e^* \;|\; e \; \mathtt{?} \; e_1 \; \mathtt{:} \; e_2 \;|\; \mathtt{[} \; e^* \; \mathtt{]} \;|\; e \mathtt{[} e \mathtt{]} \\ s\!: \mathsf{Statement} = \mathtt{let}\;i = e \;|\; \mathtt{func}\;i\;i^*=e \;|\; i = e \;|\; \mathtt{print}\;e \;|\; \mathtt{while}\;e\;b \\ b\!: \mathsf{Block} = \mathtt{block}\; s^* \\ p\!: \mathsf{Program} = \mathtt{program}\; b \end{array} $

    The unary operators are - and !. The binary operators are +, -, *, /, %, **, <, <=, ==, !=, >=, >, &&, and ||.

    The standard library is as we saw in class.

    Hint: This is exactly the extension of Bella that we saw in class when we began our study of types in operational semantics. The entire operational semantics for this language extension is provided in the course notes; you are encouraged to build your denotational semantics by simply “translating” the operational semantics to denotational semantics. (This helps to achieve the learning objective of gaining comfort with the similarities and differences between the two forms of semantic description.)

  2. Write an interpreter for the Bella extension of the previous problem, using the semantic rules you gave in your answer. The input to the interpret function can be a Bella structured program representation (an abstract syntax “tree” object). As this is not a compiler writing class, there is no need to write a parser. If you wish to write a parser, I won’t stop you, but I will not be awarding any extra credit for doing so.

    You may write your interpreter in TypeScript, any ML dialect, Clojure, Rust, Swift, Kotlin, or Java. If you choose Java, you must use records and sealed classes and any other modern Java feature, such as switch expressions, that make sense. This is after all a University course so you should be working with modern features.

    If you would like to use TypeScript, I have a skeleton of code you can use to get started:

    type BuiltInFunction = (...args: Value[]) => Value;
    type UserFunction = [Identifier[], Expression];
    export type Value = number | boolean | Value[] | BuiltInFunction | UserFunction;
    
    type Memory = Map<string, Value>;
    type Output = Value[];
    type State = [Memory, Output];
    
    // Custom type guards
    
    function isUserFunction(v: Value): v is UserFunction {
      return Array.isArray(v) && Array.isArray(v[0]) && v[0].length === 2;
    }
    
    function isBuiltInFunction(v: Value): v is BuiltInFunction {
      return typeof v === "function";
    }
    
    function isArray(x: Value): x is Value[] {
      return Array.isArray(x);
    }
    
    // Expressions
    
    export interface Expression {
      interpret(m: Memory): Value;
    }
    
    export class Numeral implements Expression {
      constructor(public value: number) {}
      interpret(_: Memory): Value {
        // TODO
      }
    }
    
    export class BooleanLiteral implements Expression {
      constructor(public value: boolean) {}
      interpret(_: Memory): Value {
        // TODO
      }
    }
    
    export class Identifier implements Expression {
      constructor(public name: string) {}
      interpret(m: Memory): Value {
        // TODO
      }
    }
    
    export class UnaryExpression implements Expression {
      constructor(public operator: string, public expression: Expression) {}
      interpret(m: Memory): Value {
        // TODO
      }
    }
    
    export class BinaryExpression implements Expression {
      constructor(
        public operator: string,
        public left: Expression,
        public right: Expression
      ) {}
      interpret(m: Memory): Value {
        // TODO
      }
    }
    
    export class Call implements Expression {
      constructor(public callee: Identifier, public args: Expression[]) {}
      interpret(m: Memory): Value {
        const functionValue = m.get(this.callee.name);
        const argValues = this.args.map((arg) => arg.interpret(m));
        if (functionValue === undefined) {
          throw new Error("Identifier was undeclared");
        } else if (isUserFunction(functionValue)) {
          const [parameters, expression] = functionValue;
          if (parameters.length !== this.args.length) {
            throw new Error("Wrong number of arguments");
          }
          const locals = parameters.map((p, i) => [p.name, argValues[i]] as const);
          return expression.interpret(new Map([...m, ...locals]));
        } else if (isBuiltInFunction(functionValue)) {
          return functionValue(...argValues);
        } else {
          throw new Error("Not a function");
        }
      }
    }
    
    export class ConditionalExpression implements Expression {
      constructor(
        public test: Expression,
        public consequent: Expression,
        public alternate: Expression
      ) {}
      interpret(m: Memory): Value {
        // TODO
    }
    
    export class ArrayLiteral implements Expression {
      constructor(public elements: Expression[]) {}
      interpret(m: Memory): Value {
        // TODO
      }
    }
    
    export class SubscriptExpression implements Expression {
      constructor(public array: Expression, public subscript: Expression) {}
      interpret(m: Memory): Value {
        // TODO
      }
    }
    
    // Statements
    
    export interface Statement {
      interpret([m, o]: State): State;
    }
    
    export class VariableDeclaration implements Statement {
      constructor(public id: Identifier, public expression: Expression) {}
      interpret([m, o]: State): State {
        // TODO
      }
    }
    
    export class FunctionDeclaration implements Statement {
      constructor(
        public id: Identifier,
        public parameters: Identifier[],
        public expression: Expression
      ) {}
      interpret([m, o]: State): State {
        // TODO
      }
    }
    
    export class Assignment implements Statement {
      constructor(public id: Identifier, public expression: Expression) {}
      interpret([m, o]: State): State {
        // TODO
      }
    }
    
    export class PrintStatement implements Statement {
      constructor(public expression: Expression) {}
      interpret([m, o]: State): State {
        return [m, [...o, this.expression.interpret(m)]];
      }
    }
    
    export class WhileStatement implements Statement {
      constructor(public expression: Expression, public block: Block) {}
      interpret([m, o]: State): State {
        // TODO
      }
    }
    
    // Block
    
    export class Block {
      constructor(public statements: Statement[]) {}
      interpret([m, o]: State): State {
        let state: State = [m, o];
        for (let statement of this.statements) {
          state = statement.interpret(state);
        }
        return state;
      }
    }
    
    // Program
    
    export class Program {
      constructor(public block: Block) {}
      interpret(): Output {
        const initialMemory: Memory = new Map<string, Value>([
          ["pi", Math.PI as Value],
          ["sqrt", Math.sqrt as Value],
          ["sin", Math.sin as Value],
          ["cos", Math.cos as Value],
          ["ln", Math.log as Value],
          ["exp", Math.exp as Value],
          ["hypot", Math.hypot as Value],
        ]);
        const [_, o] = this.block.interpret([initialMemory, []]);
        return o;
      }
    }
    
    export function interpret(p: Program) {
      return p.interpret();
    }
    

    An invocation of the interpreter might look like this:

    const sample: Program = new Program(
      new Block([new PrintStatement(new Numeral(5))])
    )
    
    interpret(sample)
    
  3. Suppose we added to the Bella extension from Problems 1 and 2 these two yet additional features: (1) type annotations on variable declarations and (2) tuple types. Do the following two things:
    1. Show the modified abstract syntax.
    2. Give the (operational) static semantics (JUST the statics, not the dynamics).

    Note the question as presented is purposely vague. In your answer, you can describe your thinking and your interpretation of the problem.