Theoretical Computer Science Package

Code
Julia
ComputerSience
Author

Daniel Schwarzenbach

Published

July 23, 2025


Build Status

This package provides implementations of various theoretical computer science concepts in Julia, including:

Installation

To install the package, use the Julia package manager:

using Pkg
Pkg.add(url="https://github.com/daniel-schwarzenbach/TheoreticalComputerScience.jl")

Usage

using TheoreticalComputerScience

Turing Machines

TMK

You can create a Turing machine with \(k\) bands.

  • no_bands: The number of bands.
  • initial_state: The initial state.
  • final_state: The final state.
  • f: Tuple{String, Vector{Char}} => Tuple{String, Vector{Char}, Vector{Movement}}

: Represents the blank symbol.

M = TMK(no_bands = 2, initial_state = "z0", final_state = "z1", f = Dict([
 #z0
 ("z0", ['a','□']) => ("z0", ['a','a'], [:R, :R]),
 ("z0", ['b','□']) => ("z0", ['b','b'], [:R, :R]),
 ("z0", ['□','□']) => ("z2", ['□','□'], [:O, :L]),
 # z2
 ("z2", ['□','b']) => ("z2", ['b','□'], [:R, :L]),
 ("z2", ['□','a']) => ("z2", ['a','□'], [:R, :L]),
 ("z2", ['□','□']) => ("z3", ['□','□'], [:L, :O]),
 # z3
 ("z3", ['a','□']) => ("z3", ['a','□'], [:L, :O]),
 ("z3", ['b','□']) => ("z3", ['b','□'], [:L, :O]),
 ("z3", ['□','□']) => ("z1", ['□','□'], [:R, :O])
 ])
)

to execute the Turing machine, you can use the execute_tm function:

execute_tm(M, "abb")

this will then execute the Turing machine on the input string “abb” and print the steps taken by the machine.

abb

□□□

⤷ f(z0,['a', '□'])  ⟶  (z0, ['a', 'a'], [R, R]) 

abb

a□□

⤷ f(z0,['b', '□'])  ⟶  (z0, ['b', 'b'], [R, R])

abb

ab□

⤷ f(z0,['b', '□'])  ⟶  (z0, ['b', 'b'], [R, R])

abb□

abb□

⤷ f(z0,['□', '□'])  ⟶  (z2, ['□', '□'], [O, L])

... (continues until the final state is reached)

TM1

You can create a Turing machine with just 1 band.

  • initial_state: The initial state.
  • final_state: The final state.
  • f: Tuple{String, Char} => Tuple{String, Char, Movement}

: Represents the blank symbol.

M = TM1(initial_state = "z0", final_state = "z1", f = Dict([
  ("z0", 'a') => ("za", '□', :R),
  ("z0", '□') => ("z1", '□', :R),
  ("z0", 'b') => ("zb", 'b', :R),
  ("za", 'a') => ("za", 'a', :R),
  ("za", 'b') => ("za", 'b', :R),
  ("za", '□') => ("z´", 'a', :L),
  ("zb", 'a') => ("za", 'a', :R),
  ("zb", 'b') => ("za", 'b', :R),
  ("zb", '□') => ("z´", 'b', :L),
  ("z´", '□') => ("z1", '□', :R),
  ("z´", 'a') => ("z´", 'a', :L),
  ("z´", 'b') => ("z´", 'b', :L)
 ])
)

you can execute the Turing machine using the execute_tm function, or with the execute_1tm function if you want to test it with a 1-∞-sided Band and simulate an 1-TM

# two-∞-sided Band
execute_tm(M, "abbabba")
# right-∞-sided Band
execute_1tm(M, "abbabba")

output will look like this:

abbabba ⟹  f(z0,a)  ⟶  (za,□,R)

□bbabba ⟹  f(za,b)  ⟶  (za,b,R)

□bbabba ⟹  f(za,b)  ⟶  (za,b,R)

□bbabba ⟹  f(za,a)  ⟶  (za,a,R)

□bbabba ⟹  f(za,b)  ⟶  (za,b,R)

□bbabba ⟹  f(za,b)  ⟶  (za,b,R)

□bbabba ⟹  f(za,a)  ⟶  (za,a,R)

□bbabba□ ⟹  f(za,□)  ⟶  (z´,a,L)

□bbabbaa ⟹  f(z´,a)  ⟶  (z´,a,L)

□bbabbaa ⟹  f(z´,b)  ⟶  (z´,b,L)

□bbabbaa ⟹  f(z´,b)  ⟶  (z´,b,L)

□bbabbaa ⟹  f(z´,a)  ⟶  (z´,a,L)

□bbabbaa ⟹  f(z´,b)  ⟶  (z´,b,L)

□bbabbaa ⟹  f(z´,b)  ⟶  (z´,b,L)

□bbabbaa ⟹  f(z´,□)  ⟶  (z1,□,R)


final state z1 reached!

Pushdown Automata

PDA come in two variants: Nondeterministic and Deterministic.

Nondeterministic PD

You can create a nondeterministic pushdown automaton (NPDA) with the following parameters:

  • initial_state: The initial state.
  • f: Tuple{String, Char, Char} => Vector{Tuple{String, String}}

ε: Represents the empty string/character.

# acept all palindromes
npda = NPDA(initial_state = "z1", f = Dict([
  ("z1", 'a', '⊥') => [("z1", "A")],
  ("z1", 'b', '⊥') => [("z1", "B")],
  ("z1", 'a', 'A') => [("z1", "AA"), ("z2", "AA")],
  ("z1", 'a', 'B') => [("z1", "BA"), ("z2", "BA")],
  ("z1", 'b', 'A') => [("z1", "AB"), ("z2", "AB")],
  ("z1", 'b', 'B') => [("z1", "BB"), ("z2", "BB")],
  ("z2", 'a', 'A') => [("z2", "")],
  ("z2", 'b', 'B') => [("z2", "")]
 ])
)

to execute the NPDA, you can use the execute_npda function:

execute_npda(npda, "aabbaa")

output will look like this:

1-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", [])
f(z1, a, ⊥)  ⟹  [("z1", "A")]

1-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", ['A'])
f(z1, a, A)  ⟹  [("z1", "AA"), ("z2", "AA")]

2-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", ['A', 'A'])
 ("z2", ['A', 'A'])
f(z1, b, A)  ⟹  [("z1", "AB"), ("z2", "AB")]

2-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", ['A', 'A', 'B'])
 ("z2", ['A', 'A', 'B'])
f(z1, b, B)  ⟹  [("z1", "BB"), ("z2", "BB")]
f(z2, b, B)  ⟹  [("z2", "")]

3-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", ['A', 'A', 'B', 'B'])
 ("z2", ['A', 'A', 'B', 'B'])
 ("z2", ['A', 'A'])
f(z1, a, B)  ⟹  [("z1", "BA"), ("z2", "BA")]
f(z2, a, A)  ⟹  [("z2", "")]

3-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", ['A', 'A', 'B', 'B', 'A'])
 ("z2", ['A', 'A', 'B', 'B', 'A'])
 ("z2", ['A'])
f(z1, a, A)  ⟹  [("z1", "AA"), ("z2", "AA")]
f(z2, a, A)  ⟹  [("z2", "")]
f(z2, a, A)  ⟹  [("z2", "")]

4-element Vector{Tuple{String, Vector{Char}}}:
 ("z1", ['A', 'A', 'B', 'B', 'A', 'A'])
 ("z2", ['A', 'A', 'B', 'B', 'A', 'A'])
 ("z2", ['A', 'A', 'B', 'B'])
 ("z2", [])
input accepted

Deterministic PDA

You can create a deterministic pushdown automaton (DPDA) with the following parameters:

  • z_0: The initial state.
  • f: Tuple{String, Char, Char} => Tuple{String, String}

ε: Represents the empty string/character.

# acept all palindromes that are separated by a '|'
dpda = DPDA(initial_state = "z1", f = Dict([
  ("z1", 'a', '⊥') => ("z1", "A"),
  ("z1", 'b', '⊥') => ("z1", "A"),
  ("z1", 'a', 'A') => ("z1", "AA"),
  ("z1", 'a', 'B') => ("z1", "BA"),
  ("z1", 'b', 'A') => ("z1", "AB"),
  ("z1", 'b', 'B') => ("z1", "BB"),
  ("z1", '|', 'A') => ("z2", "A"),
  ("z1", '|', 'B') => ("z2", "B"),
  ("z2", 'a', 'A') => ("z2", ""),
  ("z2", 'b', 'B') => ("z2", "")
 ])
)

to execute the DPDA, you can use the execute_dpda function:

execute_dpda(dpda, "aab|baa")

output will look like this:

f(z1, a, ⊥)  ⟹  z1, A
Stack: ['A']

f(z1, a, A)  ⟹  z1, AA
Stack: ['A', 'A']

f(z1, b, A)  ⟹  z1, AB
Stack: ['A', 'A', 'B']

f(z1, |, B)  ⟹  z2, B
Stack: ['A', 'A', 'B']

f(z2, b, B)  ⟹  z2,
Stack: ['A', 'A']

f(z2, a, A)  ⟹  z2,
Stack: ['A']

f(z2, a, A)  ⟹  z2,
Stack: Char[]

Final state: z2, Stack: Char[]
input accepted

Finite Automata (EA)

EA come in two variants: Nondeterministic and Deterministic.

Deterministic EA

You can create a deterministic finite automaton (DFA) with the following parameters:

  • initial_state: The initial state.
  • accepting_states: The accepting states.
  • f: Tuple{String, Char} => String
# regex: a*b+
AB = DEA(initial_state = "za", accepting_states = ["zb"], f = Dict([
  ("za", 'a') => "za",
  ("za", 'b') => "zb",
  ("zb", 'b') => "zb"
 ])
)

to execute the DFA, you can use the execute_dea function:

execute_dea(AB, "aabbbb")

output will look like this:

f(za,a)  ⟹  za
f(za,a)  ⟹  za
f(za,b)  ⟹  zb
f(zb,b)  ⟹  zb
f(zb,b)  ⟹  zb
f(zb,b)  ⟹  zb
Final state: zb
input accepted

Nondeterministic EA

You can create a nondeterministic finite automaton (NFA) with the following parameters:

  • initial_state: The initial state.
  • accepting_states: The accepting states.
  • f: Tuple{String, Char} => Vector{String}
# regex: c+ | a+c*b* | a+b*c*
ABC = NEA(initial_states = ["za"], accepting_states = ["zb","zc"], f = Dict([
  ("za", 'a') => ["za", "zb", "zc"],
  ("zb", 'b') => ["zb"],
  ("zb", 'c') => ["zc"],
  ("zc", 'c') => ["zc"],
  ("za", 'c') => ["zb", "zc"]
 ])
)

to execute the NFA, you can use the execute_nea function:

execute_nea(ABC, "aaabbc")

output will look like this:

f(["za"],a)  ⟹  ["za", "zb", "zc"]
f(["za", "zb", "zc"],a)  ⟹  ["za", "zb", "zc"]
f(["za", "zb", "zc"],a)  ⟹  ["za", "zb", "zc"]
f(["za", "zb", "zc"],b)  ⟹  ["zb"]
f(["zb"],b)  ⟹  ["zb"]
f(["zb"],c)  ⟹  ["zc"]
input accepted

Random Access Machines (RAM)

You can create a random access machine (RAM) and execute them:

test.ram:

0   R2 ← 1
1   R1 ← R0
2   R0 ← 0
3   IF R1 = 0 GOTO 8
4   R1 ← R1 − R0
5   R0 ← R0 + R2
6   R1 ← R1 − R0
7   GOTO 3

One can execute the File with:

set_registers!([9, 0, 0, 0, 0])
execute_ramfile("./test.ram")

output will look like this:

0: R2 ← 1              ⟹  R = [9, 0, 1, 0, 0]
1: R1 ← R0             ⟹  R = [9, 9, 1, 0, 0]
2: R0 ← 0              ⟹  R = [0, 9, 1, 0, 0]
3: IF R1 = 0 GOTO 8    ⟹  R = [0, 9, 1, 0, 0]
4: R1 ← R1 − R0        ⟹  R = [0, 9, 1, 0, 0]
5: R0 ← R0 + R2        ⟹  R = [1, 9, 1, 0, 0]
6: R1 ← R1 − R0        ⟹  R = [1, 8, 1, 0, 0]
7: GOTO 3              ⟹  R = [1, 8, 1, 0, 0]
3: IF R1 = 0 GOTO 8    ⟹  R = [1, 8, 1, 0, 0]
4: R1 ← R1 − R0        ⟹  R = [1, 7, 1, 0, 0]
5: R0 ← R0 + R2        ⟹  R = [2, 7, 1, 0, 0]
6: R1 ← R1 − R0        ⟹  R = [2, 5, 1, 0, 0]
7: GOTO 3              ⟹  R = [2, 5, 1, 0, 0]
3: IF R1 = 0 GOTO 8    ⟹  R = [2, 5, 1, 0, 0]
4: R1 ← R1 − R0        ⟹  R = [2, 3, 1, 0, 0]
5: R0 ← R0 + R2        ⟹  R = [3, 3, 1, 0, 0]
6: R1 ← R1 − R0        ⟹  R = [3, 0, 1, 0, 0]
7: GOTO 3              ⟹  R = [3, 0, 1, 0, 0]
3: IF R1 = 0 GOTO 8    ⟹  Final register state: [3, 0, 1, 0, 0]

One can also write a RAM program in Julia and execute it:

set_registers!([8, 4, 0, 0])
execute_ramcode([
 # ⌊R0 / R1⌋
 "IF R1 = 0 GOTO 7", # 0
 "R2 <- 1", # 1
 "R0 <- R0 + R2", # 2
 "R0 <- R0 - R1", # 3
 "R3 <- R3 + R2", # 4
 "IF R0 = 0 GOTO 7", # 5
 "GOTO 3", # 6
 "R0 <- R3 - R2", # 7
 "STOP" # 8
])

Dyadic Codes

Dyadic codes are a way to represent natural numbers in a binary-like format that is bijective. The package provides functions to convert between dyadic codes and integers.

x dya(x)
0 ε
1 1
2 2
3 11
4 12
> dya(0)
"ε"
> dya⁻¹("222")
14

Count Functions

n2nxn is a bijective function that maps natural numbers to pairs of natural numbers, and nxn2n is its inverse.

> n2nxn(5)
(2, 1)
> nxn2n(2, 1)
5