Semantics

Let's give meaning to it all.

Christian Gram Kalhauge

Table of Contents

  1. Preliminaries: Natural Deduction
  2. What are Semantics?
  3. Java Bytecode
  4. What are the Semantics of the JVM?

Questions

  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?

Preliminaries: Natural Deduction (ยง1)

Gentzen-style proofs

๐‘๐‘Ÿ๐‘’๐‘š๐‘–๐‘ 1โ€ฆ๐‘๐‘Ÿ๐‘’๐‘š๐‘–๐‘ n๐‘๐‘œ๐‘›๐‘๐‘™๐‘ข๐‘ ๐‘–๐‘œ๐‘›(name)
ABAโˆงB(โˆง)AAโˆจB(โˆจL)BAโˆจB(โˆจR)

What are Semantics? (ยง2)

  1. Axiomatic Semantics
  2. Denotational Semantics
  3. Operational Semantics
  4. Transition System and Traces

It's Meaning

Axiomatic Semantics (ยง2.1)

Meaning by specification

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

Hoare Triplet

{P}C{Q}

Example: Composition

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

Denotational Semantics (ยง2.2)

Meaning by mapping

A simple expression language

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

Mapped to Math

โ„ฐ:๐”ผ๐•โ†’(ฮฃโ†’โ„•)
โ„ฐโŸฆ๐š—โŸงฯƒ=toNat(โŸฆ๐š—โŸง)โ„ฐโŸฆ๐šกโŸงฯƒ=lookup(โŸฆ๐šกโŸง,ฯƒ)โ„ฐโŸฆ๐šŽ๐Ÿท+๐šŽ๐ŸธโŸงฯƒ=โ„ฐโŸฆ๐šŽ๐ŸทโŸงฯƒ+โ„ฐโŸฆ๐šŽ๐ŸธโŸงฯƒ

Example: x + 5

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

Operational Semantics (ยง2.3)

Meaning by execution

Natural or Big-Step

ฯˆโŠขฯƒโ†“v

Structural (SOS) or Small-Step

ฯˆโŠขฯƒโ†’ฯƒโ€พ

Beta Reduction in Natural

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

Beta Reduction in SOS

ฯˆโŠขe1โ†’eโ€พ1ฯˆโŠขe1e2โ†’eโ€พ1e2(ฮฒ1)
(ฯˆ1|e1)โˆ’an expression e1 in scope ฯˆ1
ฯˆโŠขe2โ†’eโ€พ2ฯˆโŠข(ฯˆ1|ฮปx.e1)e2โ†’(ฯˆ1|ฮปx.e1)eโ€พ2(ฮฒ2)
ฯˆโŠข(ฯˆ1|ฮปx.e1)vโ†’(ฯˆ1[xโ†ฆv]|e1)(ฮฒ3)

But there is more...

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

SOS is more expressive than NOS

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

Transition System and Traces (ยง2.4)

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.

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

Example: The language of halt

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

Java Bytecode (ยง3)

$ javap -cp target/classes -c jpamb.cases.Simple
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 }
]
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.
$ uv run jpamb inspect "jpamb.cases.Simple.assertBoolean:(Z)V"

--format=...

Building a Bytecode Syntactic Analysis (ยง3.1)

Look at the solutions/syntactic/src/syntactic_bytecode.py

What are the Semantics of the JVM? (ยง4)

Follow along in solutions/dynamic and src/jvm/state.py

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

The Context and the Program Counter (ยง4.1)

ฮน=โŸจฮนm,ฮนoโŸฉฮน+n=โŸจฮนm,ฮนo+nโŸฉฮนโ†n=โŸจฮนm,nโŸฉ
>>> pc = PC(method, offset)
>>> pc += n # add n to the offset
>>> pc %= n # replace the offest with n

Using the JPAMB suite

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

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

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

The Operator Stack

  • ฯƒโˆˆ๐•ฯƒ*

  • ฯƒ=ฯต(๐š’๐š—๐š1)(๐š’๐š—๐š2)(๐š’๐š—๐š3)

class OperandStack:
    operands: deque[StackValue]

The Locals

  • ฮปโˆˆโ„•โ†’๐•ฯƒ+โŠฅ

  • ฮป[10]

class Locals:
   locals: list[StackValue | None]

A Frame

โŸจฮป,ฯƒ,ฮนโŸฉ
class Frame:
    locals: Locals
    stack: OperandStack
    pc: PC

The Stepping Function (ยง4.3)

๐š‹๐šŒโŠขโŸจฮป,ฯƒ,ฮนโŸฉโ†’โŸจฮปโ€พ,ฯƒโ€พ,ฮนโ€พโŸฉ
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

Implementing (๐š™๐šž๐šœ๐š‘:๐™ธ v)

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

Implementing (๐š•๐š˜๐šŠ๐š:๐™ธ n)

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

ฮผโˆผโ€ฆโŸจฮป2,ฯƒ2,ฮน2โŸฉโŸจฮป1,ฯƒ1,ฮน1โŸฉ
ฮทโˆˆโ„•โ†’๐•ฮท
โŸจฮท,ฮผโŸฉโˆˆ๐’๐ญ๐š๐ญ๐ž
class State:
    heap: Heap
    frames: CallStack

Do you even lift?

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

We terminate with...

ok or err(โ€˜๐š›๐šŽ๐šŠ๐šœ๐š˜๐š—โ€™)

Terminating with ok

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

In Python

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"

Terminating with err(โ€˜..โ€™)

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

In Python

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

In Python

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)

It's now up to you!

How can I figure out what everything means?

  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.

Write an interpreter

$ jpamb -vv interpret --filter Simple --step-wise dynamic-interpreter
  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.

Next time, we will turn the interpreter into an analysis!

Questions

  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?