Semantics

Let's give meaning to it all.

Christian Gram Kalhauge

Table of Contents
  1. Preliminaries: Natural Deductionยง1
    1. What are Semantics?ยง2
      1. Axiomatic Semanticsยง2.1
        1. Denotational Semanticsยง2.2
          1. Operational Semanticsยง2.3
            1. Transition System and Tracesยง2.4
            2. Java Bytecodeยง3
              1. Building a Bytecode Syntactic Analysisยง3.1
              2. What are the Semantics of the JVM?ยง4
                1. The Context and the Program Counterยง4.1
                  1. The Values, Operator Stack, and Locals.ยง4.2
                    1. The Stepping Functionยง4.3
                      1. The Call Stack, The Heap, and Terminating the Programยง4.4
                        1. What about the rest?ยง4.5

                        Preliminaries: Natural Deduction ยง1

                        If you are unfamiliar with Natural Deduction and Gentzen-style proofs, please refer to the Wikipedia page on the topic. The short story is that we refer to logical rules like this:

                        ๐‘๐‘Ÿ๐‘’๐‘š๐‘–๐‘ 1โ€ฆ๐‘๐‘Ÿ๐‘’๐‘š๐‘–๐‘ n๐‘๐‘œ๐‘›๐‘๐‘™๐‘ข๐‘ ๐‘–๐‘œ๐‘›(name)

                        Which means that ๐‘๐‘Ÿ๐‘’๐‘š๐‘–๐‘ 1โˆงโ€ฆโˆง๐‘๐‘Ÿ๐‘’๐‘š๐‘–๐‘ n implies ๐‘๐‘œ๐‘›๐‘๐‘™๐‘ข๐‘ ๐‘–๐‘œ๐‘›.

                        If we want multiple ways of reaching the conclusion, we can make more rules. For example, conjunction AโˆงB only requires one rule, both A and B have to be true, but the disjunction AโˆจB has two rules: either A has to be true or B has to be true.

                        ABAโˆงB(โˆง)AAโˆจB(โˆจL)BAโˆจB(โˆจR)

                        In natural deduction, we build up syntactic objects which represent the truth of some event. We call them (judgements). Many times you will see them written like xโŠขy, which is read: the context of x $ implies y $ is true.

                        What are Semantics? ยง2

                        Today we are going to talk about semantics. Program semantics involves assigning meaning to programs. When we can talk about what a piece of syntax means, it is easier to explain what a program does.

                        We are going to discuss some different approaches to writing down the semantics of a program. They all essentially turn a program's syntax into mathematical logic.

                        Axiomatic Semantics ยง2.1

                        One of the first approaches invented to assign meaning to programs was Axiomatic Semantics. Here, the meaning of the program is described by assigning preconditions and postconditions to all statements in a program.

                        The flowchart from the original paper on semantics Assigning Meaning to Programs by Robert W. Floyd. The program computes the sum of an array.

                        We can describe the semantics of a program by denoting three parts, the precondition P, the program C, and the postcondition Q, this is also called Hoare triplets.

                        {P}C{Q}

                        A Hoare triplet means that if the world satisfies P before executing C, then the world will satisfy Q after. We can compose the proofs of correctness of program parts into a proof of total correctness. Assume a program C1;C2, where C2 is executed after C1. Then we can describe the correctness of C1;C2, like so:

                        {P1}C1{Q1}{P2}C2{Q2}Q1โ‡’P2{P1}C1;C2{Q2}

                        If you are unfamiliar with the syntax above, it is natural deduction; see the section on natural deduction. Given {P1}C1{Q1}, {P2}C2{Q2}, and the implication that the postcondition of C1 $ implies the precondition of C2 (Q1โ‡’P2) are true, we can also prove that {P}C1;C2{Q}.

                        This approach is effective at describing the meaning of specific programs. This approach excels at program verification but is not typically used for program analysis in general.

                        Denotational Semantics ยง2.2

                        Another approach to defining program semantics is Denotational Semantics. Denotational semantics maps the semantics of a program to something well-understood. This can be another programming language or mathematics.

                        Consider a very simple expressional language called ๐”ผ๐•, which has addition, variables x and natural numbers n:

                        eโˆˆ๐”ผ๐•:=e1+e2|x|n

                        If you want to give meaning to the ๐”ผ๐• program x + 5, we can define a map from expressions to a function from a store to integers: โ„ฐ:๐”ผ๐•โ†’(ฮฃโ†’โ„•)

                        โ„ฐโŸฆ๐š—โŸงฯƒ=toNat(โŸฆ๐š—โŸง)โ„ฐโŸฆ๐šกโŸงฯƒ=lookup(โŸฆ๐šกโŸง,ฯƒ)โ„ฐโŸฆ๐šŽ๐Ÿท+๐šŽ๐ŸธโŸงฯƒ=โ„ฐโŸฆ๐šŽ๐ŸทโŸงฯƒ+โ„ฐโŸฆ๐šŽ๐ŸธโŸงฯƒ

                        Essentially, โ„ฐ gives meaning to numbers, variables, and addition. Numbers should be read as natural numbers. Variables correspond to looking up a value in a store. The semantic + is equivalent to the mathematical operator +.

                        Now we can see that x + 5 can be calculated in the store ฯƒ=[xโ†ฆ3], using normal math:

                        โ„ฐโŸฆ๐šก+๐ŸปโŸงฯƒ=โ„ฐโŸฆ๐šกโŸงฯƒ+โ„ฐโŸฆ๐ŸปโŸงฯƒ=lookup(โŸฆ๐šกโŸง,ฯƒ)+โ„ฐโŸฆ๐ŸปโŸงฯƒ=3+โ„ฐโŸฆ๐ŸปโŸงฯƒ=3+toNat(โŸฆ๐ŸปโŸง)=3+5=8

                        If you think this looks exactly like functional programming, you would be right. It is also particularly well-suited for describing Expression-oriented programming languages.

                        Operational Semantics ยง2.3

                        Next, we cover Operational Semantics, the semantic style we will focus on this semester. Operational semantics describes the semantics of a program as changes to a state. This makes it ideal for describing imperative languages like JVM bytecode. Furthermore, the Structural Operational Semantics is defined as you would write an interpreter, which is handy because you are going to write one.

                        The Structural Operational Semantics or Small Step Semantics is written as a judgment of the type (ฯˆโŠขฯƒโ†’ฯƒโ€พ), indicating that given the environment ฯˆ, the program state ฯƒ transitions to ฯƒโ€พ.

                        We call this approach small-step semantics because we only execute a single operation at a time.

                        The Natural Operational Semantics or Big Step Semantics describes running the program until it halts. (ฯˆโŠขฯƒโ†“v) where v is the final value of the program. Big step semantics often appears simpler than small step semantics because it ignores the intermediate execution order.

                        Lambda Calculus

                        Consider the application rule of lambda calculus (see here and Lambda calculus - Wikipedia).

                        ฯˆโŠขe1โ†“ฮปx.e3ฯˆโŠขe2โ†“v2ฯˆ[xโ†ฆv2]โŠขe3โ†“vฯˆโŠขe1e2โ†“v(โ†“ฮฒ)

                        Here ฯˆ contains a mapping from variables to values, which we can update using the ฯˆ[xโ†ฆv] operation.

                        Essentially, this rule states that function application e1e2 computes a v in ฯˆ if e1 computes a closure (ฮปx.e3), e2 $ computes a value v2, and e3 computes v in the environment where x $ is set to v2. Compare this with the single-step semantics. We start out simple, compute on the left side until no more progress can be made:

                        ฯˆโŠขe1โ†’eโ€พ1ฯˆโŠขe1e2โ†’eโ€พ1e2(ฮฒ1)

                        Because we can no longer do the full computation in one go, we have to carry around the partial state of the experession. To this end we have to have to redefine what a expression is.

                        (ฯˆ1|e1)โˆ’an expression e1 in scope ฯˆ1

                        Now we say that if we have evaluated left hand side until a value has been reached we calculate on the right side.

                        ฯˆโŠขe2โ†’eโ€พ2ฯˆโŠข(ฯˆ1|ฮปx.e1)e2โ†’(ฯˆ1|ฮปx.e1)eโ€พ2(ฮฒ2)

                        Finally, we include the mapping in the left hand side.

                        ฯˆโŠข(ฯˆ1|ฮปx.e1)vโ†’(ฯˆ1[xโ†ฆv]|e1)(ฮฒ3)

                        Here we have three rules instead of one. First, in ฮฒ1, we step the function e1, then in ฮฒ2 we step the argument e2, and finally we insert the value v into the closure ฯˆ1. Notice that we also have to keep track of the variables captured by the closure using the (ฯˆ1|e2) notation. We also need to add a closure evaluation rule:

                        ฯˆโ‹…ฯˆ1โŠขe1โ†’eโ€พ1ฯˆโŠข(ฯˆ1|e1)โ†’(ฯˆ1|e1โ€พ)(ฯˆ1)ฯˆโ‹…ฯˆ1โŠขe1โ†’vฯˆโŠข(ฯˆ1|e1)โ†’v(ฯˆ2)

                        Big Step semantics have the benefit of being easier to read; however, they have disadvantages, namely: we cannot reason about programs that run forever, and we cannot turn big step semantics into a working implementation. In contrast, small step semantics are easy to convert into an interpreter, and we can always recover the big step semantics from operational semantics by simply applying the single step semantics until the program terminates with a value:

                        ฯˆโŠขฯƒโ†’ฯƒโ€พฯˆโŠขฯƒโ€พโ†“vฯˆโŠขฯƒโ†“v(step)ฯˆโŠขฯƒโ†’vฯˆโŠขฯƒโ†“v(done)

                        Transition System and Traces ยง2.4

                        With our newfound definition of single-step semantics, we can define the meaning of a program P as a Transition System:

                        A Transition System

                        A Transition system is a triplet โŸจ๐’๐ญ๐š๐ญ๐žp,ฮดp,IpโŸฉ where ๐’๐ญ๐š๐ญ๐žp is the set of program states, ฮดp is the transition relation (defined by the single step semantics) and Ip are possible initial states.

                        A ๐“๐ซ๐š๐œ๐žp is the possible infinite sequence of states and operations of the program.

                        ๐“๐ซ๐š๐œ๐žpโІ๐’๐ญ๐š๐ญ๐žpโ‹†

                        The meaning of a program is now the set of traces that it exhibit:

                        Sem:๐๐ซ๐จ๐ ๐ซ๐š๐ฆโ†’2๐“๐ซ๐š๐œ๐žSem(p)={ฯ„โˆˆ๐’๐ญ๐š๐ญ๐žpnย |ย nโˆˆ[1,โˆž],ฯ„0โˆˆIp,โˆ€iโˆˆ[1,nโˆ’1],ฮดp(ฯ„iโˆ’1,ฯ„i)}

                        This is also called the Maximal Trace Semantics. We can now define properties, like does a program halt, using relatively well defined math:

                        โ„’halt={pย |ย ⁡pโˆˆโ„’,โˆ€ฯ„โˆˆSem(p).|ฯ„|โ‰ โˆž}

                        But more about this next time.

                        Java Bytecode ยง3

                        Before we can start assigning meaning to the JVM bytecode, we need to understand what JVM bytecode is. There are many different instruction architectures, such as X86, ARM, and RISC-V. If you want to support them all, you have to produce machine code for each. Java, however, wanted to provide a write once, run everywhere solution, so they needed a virtual machine. A virtual machine is a piece of code written for each platform that all run the same virtual machine code. Actually, JVM stands for Java Virtual Machine.

                        When Java is compiled it gets compiled into a format known as JVM bytecode and placed in files with the .class extension. These files are called class files.

                        Inspect the class file

                        In the jpamb folder you should be able to run the following command to see the content of the target/classes/jpamb/cases/Simple.class file.

                        $ javap -cp target/classes -c jpamb.cases.Simple

                        Each classfile contains the class name, its fields, and its methods. Each method contains a list of executable instructions. JVM bytecode is designed to be easy to interpret, which can make it hard to read. In this course, we will use an intermediate language that reduces the two hundred or so instructions to 39 and performs several useful optimizations.

                        You should be able to find decompiled versions of each class file in the target/decompiled/ folder, in an easy to read JSON format (see codec here).

                        Inspect the Bytecode

                        Examine the decompiled cases, starting with Simple.json. Locate the assertFalse method listed in the methods. Find the code.bytecode section, which should look like:

                        jq '.methods[] | select(.name=="assertFalse") | .code.bytecode' \
                          target/decompiled/jpamb/cases/Simple.json
                        [ 
                        { "opr": "get", "field": { "class": "jpamb/cases/Simple", "name": "$assertionsDisabled", "type": "boolean" },  "static": true },
                        { "opr": "ifz", "condition": "ne", "target": 6 },
                        { "opr": "new", "class": "java/lang/AssertionError" },
                        { "opr": "dup", "words": 1 },
                        { "opr": "invoke", "access": "special", "method": { "args": [], "is_interface": false, "name": "<init>", "ref": { "kind": "class", "name": "java/lang/AssertionError" }, "returns": null },  },
                        { "opr": "throw" },
                        { "opr": "return", "type": null }
                        ]

                        Especially, notice the opr, it explains what operations to run:

                        in#oprstackdescription
                        00get[]Get the assertionsDisabled boolean and put it on the stack
                        01ifz[bool]if it is not equal to zero (false) jump to the 6th instruction (i.e. return)
                        02new[]otherwise create a new AssertionError object.
                        03dup[ref]dublicate the reference
                        04invoke[ref, ref]call the init method on the AssertionErrror (consuming the top ref erence).
                        05throw[ref]throw the assertion error.
                        06return[]otherwise return.

                        Finally, have we built Python classes for all the instructions used in the benchmark suite? You can see them here. To parse each bytecode, you can either use the jpamb.jvm.Opcode.from_json function for each instruction or use the jpamb.Suite.method_opcodes(methodid, eff=eff) to get the opcodes from an absolute method ID.

                        Compare and contrast

                        Try to run the inspect command:

                        $ uv run jpamb inspect "jpamb.cases.Simple.assertBoolean:(Z)V"

                        You can also try the other formats --format=repr (shows the python code), --format=real (shows the JVM memonic), and --format=json (shows the decompiled json).

                        Building a Bytecode Syntactic Analysis ยง3.1

                        Now you should be able to build a syntactic analysis over the bytecode:

                        (Optional) Build a Bytecode Syntactic Analysis

                        Start by taking a look at the solutions/syntactic/src/syntactic_bytecode.py.

                        Then try to expand it to be more precise.

                        What are the Semantics of the JVM? ยง4

                        In this section, we introduce the semantics of a limited JVM. As it is incomplete, you will need to complete it yourself.

                        The goal of this section is to define the small step semantics of the JVM, and encode it in both math and Python. You can follow along with code in the dynamic Solution and the defintion of the state in jvm.state.

                        We want to define judgments of bcโŠขsโ†’sโ€พ, bcโŠขsโ†’ok, or bcโŠขsโ†’err(โ€˜๐š–๐šœ๐šโ€™), where bc is the bytecode and s is the state. In Python, we want to define a function step, which, given a state s, computes either a new state s or a done string.

                        def step(bc: Bytecode, s: State) -> State | str:
                            ...

                        The Context and the Program Counter ยง4.1

                        As with all single-step semantic rules, the JVM runs in a context. This context is the bytecode ๐š‹๐šŒ. A simple operation ๐š‹๐šŒ[ฮน] looks up the bytecode instruction at ฮน. ฮน is the program counter, i.e., the name of the method and the offset in that method.

                        We use the following shorthands:

                        ฮน=โŸจฮนm,ฮนoโŸฉฮน+n=โŸจฮนm,ฮนo+nโŸฉฮนโ†n=โŸจฮนm,nโŸฉ

                        To do this in Python, you can use PC class from jvm.state

                        class PC:
                            method: jvm.AbsMethodID
                            offset: int
                        
                            ...

                        We can change offset using the following command:

                        >>> pc = PC(method, offset)
                        >>> pc += n # add n to the offset
                        >>> pc %= n # replace the offest with n

                        To access the opcode, use the program counter as the index to the bytecode. Before that, we must access the bytecode using specific procedures.

                        >>> suite, eff = jpamb.setup()
                        >>> bc = jpamb.Bytecode(suite, eff, {})
                        >>> bc[pc]

                        The Values, Operator Stack, and Locals. ยง4.2

                        The JVM is a stack-based virtual machine; this means that instead of using named variables (registers) to store intermediate values, it uses an operator stack. Some values are stored in local method storage called locals, which can be accessed using indices. Finally, the machine can also store information in global memory, referred to as the heap.

                        The values in (our interpretation of) the JVM are dynamically typed; this means that every value carries information about its type. There are two kinds of values, stack values ๐•ฯƒ and heap values ๐•ฮท.

                        ๐•ฯƒ:=(๐š’๐š—๐šn)|(๐š๐š•๐š˜๐šŠ๐šf)|(๐š›๐šŽ๐šr)๐•ฮท:=๐•ฯƒ|(๐š‹๐šข๐š๐šŽb)|(๐šŒ๐š‘๐šŠ๐š›c)|(๐šœ๐š‘๐š˜๐š›๐šs)|(๐šŠ๐š›๐š›๐šŠ๐šขta)|(๐š˜๐š‹๐š“๐šŽ๐šŒ๐šcnfs)

                        The stack contains signed 32-bit integers, 32-bit floating-point values (IEEE 754 Standard (JLS ยง1.7)), and 32-bit references to the heap. The heap can contain values from the stack, 8-bit bytes, 16-bit unsigned chars, signed 16-bit shorts, arrays (consisting of a type and a list of values), and objects (comprising a name and a mapping of field names to heap values).

                        In this course, we won't cover long and double to avoid unnecessary complexity. Furthermore, we'll try to avoid inner classes and bootstrap methods.

                        Implementations of the types are stored in the jvm.type.

                        The stack is a list of values: ฯƒ=๐•ฯƒ*. ฯต denotes the empty stack, and we add and remove elements from the end of the stack. A stack with the integers 1, 2, and 3 looks like this: ฯƒ=ฯต(๐š’๐š—๐š1)(๐š’๐š—๐š2)(๐š’๐š—๐š3). A simple implementation of this in Python is shown below:

                        class OperandStack:
                            operands: deque[StackValue]

                        The JVM saves local variables of type ๐•ฯƒ to a local array ฮป. This array contains the method inputs and any data stored on the stack instead of the heap. The local array is indexed ฮป[0].

                        class Locals:
                           locals: list[StackValue | None]

                        In its simplest form, the JVM state is a triplet โŸจฮป,ฯƒ,ฮนโŸฉ called a frame, where ฮป represents the local variables, ฯƒ is the operand stack, and ฮน is the program counter.

                        In Python, we would write:

                        class Frame:
                            locals: Locals
                            stack: OperandStack
                            pc: PC

                        The Stepping Function ยง4.3

                        Most of our operations only operate on the frame, so we can already define our first simple SOS judgment like this:

                        ๐š‹๐šŒโŠขโŸจฮป,ฯƒ,ฮนโŸฉโ†’โŸจฮปโ€พ,ฯƒโ€พ,ฮนโ€พโŸฉ

                        Anticipating the definition of State in the next section, we define the stepping function, like so:

                        def step(bc: jpamb.Bytecode, state: State) -> tuple[PC, State | str]:
                            frame = state.frames.peek()
                            opr = bc[frame.pc]
                            pc, output = frame.pc, state  # Default to outputting the state
                            print(f"Stepping {pc}:\n > {opr}", file=sys.stderr)
                            match opr:
                                # The opcodes to handle ...
                            return pc, output

                        We are now ready for some examples. First we have the push operation (๐š™๐šž๐šœ๐š‘:๐™ธ v):

                        ๐š‹๐šŒ[ฮน]=(๐š™๐šž๐šœ๐š‘:๐™ธ v)๐š‹๐šŒโŠขโŸจฮป,ฯƒ,ฮนโŸฉโ†’โŸจฮป,ฯƒ(๐š’๐š—๐šv),ฮน+1โŸฉ(pushI)

                        Which we can encode in Python like this:

                        case jvm.Push(type=t, value=v):
                            if t is not jvm.Int():
                                raise NotImplementedError(f"Don't know how to handle {t}")
                            frame.stack.push(StackInt(v))
                            frame.pc += 1

                        We can also load values from the locals using the (๐š•๐š˜๐šŠ๐š:๐™ธ n) operation:

                        ๐š‹๐šŒ[ฮน]=(๐š•๐š˜๐šŠ๐š:๐™ธ n)(๐š’๐š—๐šv)=ฮป[n]๐š‹๐šŒโŠขโŸจฮป,ฯƒ,ฮนโŸฉโ†’โŸจฮป,ฯƒ(๐š’๐š—๐šv),ฮน+1โŸฉ(loadI)

                        Which we can encode in Python like this:

                        case jvm.Load(type=jvm.Int(), index=n):
                            v = frame.locals[n]
                            frame.stack.push(v)
                            frame.pc += 1

                        The Call Stack, The Heap, and Terminating the Program ยง4.4

                        In the JVM methods are capable of calling other methods. To support this we need a stack of frames, or a call stack ฮผ:

                        ฮผโˆผโ€ฆโŸจฮป2,ฯƒ2,ฮน2โŸฉโŸจฮป1,ฯƒ1,ฮน1โŸฉ

                        And now since we have multiple frames, we also need a way for the frames to share data. We call that the heap ฮท. And it is just mapping from memory locations to ๐•ฮท.

                        ฮทโˆˆโ„•โ†’๐•ฮท

                        The state is now just a tuple of the heap and the call stack: โŸจฮท,ฮผโŸฉ. Which we can represent in Python like this:

                        class State:
                            heap: Heap
                            frames: CallStack

                        So now we have reached the correct definition of the stepping function, over the State.

                        ๐š‹๐šŒโŠขโŸจฮท,ฮผโŸฉโ†’โŸจฮทโ€พ,ฮผโ€พโŸฉ

                        We can use the operations we defined over frames, by lifting them into the state, by doing the frame operation on the top frame:

                        ๐š‹๐šŒโŠขโŸจฮป,ฯƒ,ฮนโŸฉโ†’โŸจฮปโ€พ,ฯƒโ€พ,ฮนโ€พโŸฉ๐š‹๐šŒโŠขฮผโŸจฮป,ฯƒ,ฮนโŸฉโ†’ฮผโŸจฮปโ€พ,ฯƒโ€พ,ฮนโ€พโŸฉ(liftฮผ)
                        ๐š‹๐šŒโŠขฮผโ†’ฮผโ€พ๐š‹๐šŒโŠขโŸจฮท,ฮผโŸฉโ†’โŸจฮท,ฮผโ€พโŸฉ(liftฮท)

                        Our simplification of the JVM terminates when we exit from the last method, or we encounter an error. We do that with either an ok or an err(โ€˜๐š›๐šŽ๐šŠ๐šœ๐š˜๐š—โ€™).

                        ๐š‹๐šŒ[ฮน]=(๐š›๐šŽ๐š๐šž๐š›๐š—:๐™ธ)๐š‹๐šŒโŠขโŸจฮท,ฯตโŸจฮป,ฯƒ(๐š’๐š—๐šv),ฮนโŸฉโŸฉโ†’ok(returnฯต)
                        ๐š‹๐šŒ[ฮน]=(๐š›๐šŽ๐š๐šž๐š›๐š—:๐™ธ)ฮผ1=โŸจฮป,ฯƒ(๐š’๐š—๐šv),ฮนโŸฉฮผ2=โŸจฮป2,ฯƒ2,ฮน2โŸฉ๐š‹๐šŒโŠขโŸจฮท,ฮผฮผ2ฮผ1โŸฉโ†’โŸจฮท,ฮผโŸจฮป2,ฯƒ2(๐š’๐š—๐šv),ฮน2+1โŸฉโŸฉ(returnฮผ)

                        In Python, we can represent terminating with no error by simply returning "ok" if the stack is empty:

                        case jvm.Return(type=jvm.Int()):
                            v1 = frame.stack.pop()
                            state.frames.pop()
                            if state.frames:
                                frame = state.frames.peek()
                                frame.stack.push(v1)
                                frame.pc += 1
                            else:
                                output = "ok"

                        One example of throwing an error is the divide operation:

                        ๐š‹๐šŒ[ฮน]=(๐š‹๐š’๐š—๐šŠ๐š›๐šข:๐™ธ ๐š๐š’๐šŸ)v2=0๐š‹๐šŒโŠขโŸจฯƒ(๐š’๐š—๐šv1)(๐š’๐š—๐šv2),ฮนโŸฉโ†’err(โ€˜๐š๐š’๐šŸ๐š’๐š๐šŽ ๐š‹๐šข ๐šฃ๐šŽ๐š›๐š˜โ€™)(bdivI0)
                        ๐š‹๐šŒ[ฮน]=(๐š‹๐š’๐š—๐šŠ๐š›๐šข:๐™ธ ๐š๐š’๐šŸ)v2โ‰ 0v3=v1/๐š’๐Ÿน๐Ÿธv2๐š‹๐šŒโŠขโŸจฯƒ(๐š’๐š—๐šv1)(๐š’๐š—๐šv2),ฮนโŸฉโ†’โŸจฯƒ(๐š’๐š—๐šv3),ฮน+1โŸฉ(bdivI1)

                        Which can be translated into a single case match in Python. In the case we terminate with an error, we just return the corresponding string:

                        case jvm.Binary(type=jvm.Int(), operant=op):
                            v2, v1 = frame.stack.pop(), frame.stack.pop()
                            assert isinstance(v1, jvmc.StackInt), f"expected int, but got {v1}"
                            assert isinstance(v2, jvmc.StackInt), f"expected int, but got {v2}"
                        
                            value: int | str = binary(op, v1.value, v2.value)
                        
                            if isinstance(value, str):
                                output = value
                            else:
                                frame.stack.push(jvmc.StackInt(value))
                                frame.pc += 1
                        def binary(op, v1: int, v2: int) -> int | str:
                            match op:
                                case jvm.BinaryOpr.Div:
                                    try:
                                        return v1 // v2
                                    except ZeroDivisionError:
                                        return "divide by zero"
                                case a:
                                    raise NotImplementedError(f"Unhandled binary {op!r}")

                        What about the rest? ยง4.5

                        So this is a very limited definition of semantics, the JVM; we have not covered threads or exceptions, and we probably won't in this course. But even with these restrictions, there are still many undefined rules left. It is your job to implement the rest of the instructions and build a working interpreter. To get started on this, we recommend you start small.

                        Write out dup as single step semantics

                        Write dup as single step semantic, given the definitions above. You can use the following resources:

                        1. jvm2json/CODEC.txt at kalhauge/jvm2json: the decompiled codec (search for <ByteCodeInst>).

                        2. List of Java bytecode instructions - Wikipedia.

                        3. Chapter 4. The class File Format: the class file format. And, finally

                        4. Chapter 6. The Java Virtual Machine Instruction Set: the official specification of each instruction.

                        Add dup to the case statement

                        Encode the dup instruction above in the solutions/dynamic/src/dynamic.py file.

                        Finally, you should write an interpreter, which can interpret all the cases.

                        Write an Interpreter

                        Write an interpreter for the JVM, that given a method id, an input, and a maximal number of iterations, prints the steps to get to a final state.

                        The defintion of the task can be found here. Make sure JPAMB is up-to-date and that you have added dynamic to the environment: (current is 0.6.0)

                        $ uv pip list
                        Package          Version Editable project location
                        ---------------- ------- -----------------------------------------------------
                        dynamic          0.1.0   .../jpamb/solutions/dynamic
                        jpamb            0.6.0   .../jpamb
                        ...

                        Hint (Hover to see)

                        To get started, try running the interpreter on the simple cases, and in step-wise fasion

                        $ jpamb -vv interpret --filter Simple --step-wise dynamic-interpreter

                        You should expect to see an output like this:

                        $ jpamb -vv interpret --filter Simple --step-wise dynamic-interpreter
                        โ”Œ Reading cases from /home/chrg/Projects/Courses/jpamb/target/stats/cases.txt
                        โ”” Reading cases from /home/chrg/Projects/Courses/jpamb/target/stats/cases.txt
                        โ”Œ Getting info about interpreter
                        โ”‚ SUCCESS Ran dynamic-interpreter info
                        โ”” Getting info about interpreter
                        โ”Œ Trying to read state from cache
                        โ”” Trying to read state from cache
                        โ”Œ Experiment 1/28 jpamb.cases.Simple.assertBoolean:(false) -> assertion error
                        โ”‚โ”Œ Run experiment dynamic-interpreter 'jpamb.cases.Simple.assertBoolean:(Z)V' '(false)' 100
                        โ”‚โ”” Run experiment dynamic-interpreter 'jpamb.cases.Simple.assertBoolean:(Z)V' '(false)' 100
                        โ”‚ WARNING Ran dynamic-interpreter 'jpamb.cases.Simple.assertBoolean:(Z)V' '(false)' 100, and got error:
                        โ”‚ (state
                        โ”‚   :heap ()
                        โ”‚   :frames (
                        โ”‚     :00 (frame
                        โ”‚       :locals (
                        โ”‚         :00 (int 0)
                        โ”‚       )
                        โ”‚       :stack ()
                        โ”‚       :pc "jpamb.cases.Simple.assertBoolean:(Z)V:0"
                        โ”‚     )
                        โ”‚   )
                        โ”‚ )
                        โ”‚ Stepping jpamb.cases.Simple.assertBoolean:(Z)V:0:
                        โ”‚  > get static jpamb.cases.Simple.$assertionsDisabled:Z
                        โ”‚ (state
                        โ”‚   :heap ()
                        โ”‚   :frames (
                        โ”‚     :00 (frame
                        โ”‚       :locals (
                        โ”‚         :00 (int 0)
                        โ”‚       )
                        โ”‚       :stack (
                        โ”‚         :00 (int 0)
                        โ”‚       )
                        โ”‚       :pc "jpamb.cases.Simple.assertBoolean:(Z)V:1"
                        โ”‚     )
                        โ”‚   )
                        โ”‚ )
                        โ”‚ Stepping jpamb.cases.Simple.assertBoolean:(Z)V:1:
                        โ”‚  > ifz ne 8
                        โ”‚ Traceback (most recent call last):
                        โ”‚   File "/home/chrg/Projects/Courses/jpamb/.jpamb-eval/bin/dynamic-interpreter", line 10, in <module>
                        โ”‚     sys.exit(interpret())
                        โ”‚              ~~~~~~~~~^^
                        โ”‚   File "/home/chrg/Projects/Courses/jpamb/solutions/dynamic/src/dynamic.py", line 136, in interpret
                        โ”‚     pc, state = step(bc, state)
                        โ”‚                 ~~~~^^^^^^^^^^^
                        โ”‚   File "/home/chrg/Projects/Courses/jpamb/solutions/dynamic/src/dynamic.py", line 80, in step
                        โ”‚     raise NotImplementedError(a.help())
                        โ”‚ NotImplementedError: It seems Ifz(offset=3, condition=<CmpOpr.Ne: 1>, target=8) is not implemented! Instructions can be found at: https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-6.html#jvms-6.5.if_cond
                        โ”‚ WARNING Invalid output: No reponse created
                        โ”” Experiment 1/28 jpamb.cases.Simple.assertBoolean:(false) -> assertion error
                        ERROR Stopping early

                        Now it states that the Ifz case has not been implement, go ahead and do that. In the large match statement, add the following code:

                        case jvm.Ifz(condition=op, target=target):
                            value = frame.stack.pop()
                            assert isinstance(value, jvmc.StackInt), f"expected int, but got {value}"
                        
                            if compare(op, value.value, 0):
                                frame.pc %= target
                            else:
                                frame.pc += 1

                        Here are some advice getting started:

                        1. Start small, one method at a time. You don't have to cover the entire language.

                        2. It's okay to hack some things, like getting the $assertionsDisabled static field. You can assume that is always be false.

                        3. Print out the state to stderr at every step, this will help you debug (already done in the example)

                        4. Use the --step-wise flag to run the last failed case next time.

                        Until next time!

                        Until next time, write the best analysis you can and upload the results to Autolab, and ponder the following:

                        1. What is the semantics of a program?

                        2. Name some ways can you describe the semantics of a program?

                        3. What does it mean to interpret a piece of code?

                        Bibliography

                        1. Floyd, Robert W . (1993). Assigning Meanings to Programs. doi:10.1007/978-94-011-1793-7_4 link

                        2. Nielson, Hanne Riis; Nielson, Flemming (2007). Semantics with Applications.

                        3. Plotkin, Gordon D (2004). The origins of structural operational semantics. doi:10.1016/j.jlap.2004.03.009 link