🍑PEACH🍑 programming language
PEACH - Prototypical language with knack for extensibility and meta programming
Team @mynameisjonas:
@emd22
@ajmd17
PEACH is a modern programming language, handwritten in both Python and itself, completely written from scratch over the past 20 days. Included is the core language, a standard library, and some examples(including TicTacToe!)
In PEACH, all types are objects, and vice versa. Everything is an object, and that includes functions. Objects descend from a parent object and inherit functions and variables from the parent. For example, to create a type, we extend from a parent Object
(in this case Type
), and define any methods needed within the type.
The core library in PEACH is written in itself, even allowing for methods to be attached with the Object.patch()
method at runtime. Types in PEACH are built of Object
s and have overloadable functions for operations such as addition, subtraction, multiplication, division, modulus, and compare.
All builtin types such as String, Int, Float, Array, Bool, etc. are all defined completely in PEACH, including all operations that can be done on them. An example of this supercharged extensibility being the call operator can be overloaded, with an example being that when a Number is called, like 5(10)
, the values are multiplied together, allowing for a more classical math-like syntax in programming.
Example:
let Person: type = Type.extend({
instance = {
name: str
# overload the | operator
func __bitor__(self, other) {
return Person.new(self.name + ' ' + other.name);
}
# example of lambdas
to_str: Func = self -> "Person [name: " + self.name + "]"
}
# constructor
func __construct__(self, name) {
self.name = name;
}
});
let person_a: Person = Person.new('Bruce');
let person_b: Person = Person.new('Wayne');
let combined: Person = person_a | person_b;
io.write_color(Console.RED, combined); # prints `Person [name: 'Bruce Wayne']` in red
Types are objects and all objects are patchable, meaning you can swap out or add methods dynamically.
Int.patch({
# overload object being called as a function.
# arguments are passed in as an array, so
# you need to splat (*) the arguments, expanding
# from the first (the first argument would be the `Int` type
# itself)
func __call__(self, args) {
return self.__mul__(*(args.from(1)));
}
});
let result: int = 10(20);
print(result); # prints 200
Later we plan to rewrite PEACH in C/C++ for better speed, efficiency. Both of us have more experience in C/C++. We plan to keep PEACH code and the standard library similar to how it is today.
Links:
Github Repository
Repl.it link(click here to run)
Documentation:
vars - How to declare and use variables
functions - Writing and calling functions
strings - Strings operations and interpolation
operators - Available operators + Operator overloading
types - Custom types
embedding - Embedding in Python
proto - Extending types using prototypical inheritance
macros - Macros
arrays - Array operations
iterators - Building custom iterator objects
random - Random number generation
modules - Modules
console - Console input and output
files - File reading and writing
Standard Library
Examples
TicTacToe Example
Be sure play the TicTacToe example for yourself!
Start the REPL by running main.py
and call tictactoe();
to try it out!
Code:
let ttt: type = Type.extend({
name = "TicTacToe"
EMPTY = 0
O_SPOT = 1
X_SPOT = 2
instance = {
moves = [0, 0, 0, 0, 0, 0, 0, 0, 0]
func to_str(self) {
return "TicToeToe moves: " + self.moves.to_str();
}
}
func __construct__(self){
}
play = func (self){}
board_gen = func (self){}
func board_draw_top(self){
}
func get(self, loc) {
let strn = loc+1;
let ch:str = strn.to_str();
let move = self.moves[loc].to_int();
if move == self.O_SPOT {
ch = 'O';
}
elif move == self.X_SPOT {
ch = 'X';
}
return ch;
}
func clear_board(self) {
self.moves = [0, 0, 0, 0, 0, 0, 0, 0, 0];
}
func ai_find_next_spot(self) {
import "std/time.peach";
import "std/math/random.peach";
let available_spaces = [];
let index = 0;
for move in self.moves {
if move == 0 {
available_spaces += index;
}
index += 1;
}
if available_spaces.len() == 0 {
self.board_draw();
print("\n=== TIED! ===\n");
self.clear_board();
self.ai_find_next_spot();
return 0;
}
let rand = random.range(Time.now().to_int(), available_spaces.len()-1, 0);
self.moves[available_spaces[rand]] = self.O_SPOT;
available_spaces = [];
for move in self.moves {
if move == 0 {
available_spaces += index;
}
index += 1;
}
if available_spaces.len() == 0 {
self.board_draw();
print("\n=== TIED! ===\n");
self.clear_board();
self.ai_find_next_spot();
return 0;
}
}
func ai_move(self) {
return self.ai_find_next_spot();
}
func check_index(self, player, center_index, offset, check_offset) {
let index = 0;
while index != 3 {
if self.moves[center_index-check_offset] == player {
if self.moves[center_index+check_offset] == player {
if self.moves[center_index] == player {
return player;
}
}
}
center_index += offset;
index += 1;
}
return self.EMPTY;
}
func check_diag_corner(self, player, top, bottom) {
if self.moves[top] == player {
if self.moves[bottom] == player {
if self.moves[4] == player {
return player;
}
}
}
return self.EMPTY;
}
func check_diagonal(self, player) {
let tl = self.check_diag_corner(player, 0, 8);
let bl = self.check_diag_corner(player, 6, 2);
if tl {
return tl;
}
if bl {
return bl;
}
return self.EMPTY;
}
func print_winner(self, player) {
if player == self.O_SPOT {
self.board_draw();
print("\n=== YOU LOSE! ===\n");
}
elif player == self.X_SPOT {
self.board_draw();
print("\n=== YOU WIN! ===\n");
}
}
func check_winner(self, player) {
# start at left side in middle of board, checking above and below, and move by 1 spot.
let colcheck = self.check_index(player, 3, 1, 3);
# start at top in middle of board, checking left and right and moving down by board width
let rowcheck = self.check_index(player, 1, 3, 1);
let diagcheck = self.check_diagonal(player);
# check if win in columns
if colcheck {
self.print_winner(player);
self.clear_board();
return true;
}
# check if win in rows
elif rowcheck {
self.print_winner(player);
self.clear_board();
return true;
}
# check if win in diagonal spaces
elif diagcheck {
self.print_winner(player);
self.clear_board();
return true;
}
return false;
}
func player_write(self, player_char) {
if player_char == 'O' {
io.write_color(Console.RED, player_char);
}
elif player_char == 'X' {
io.write_color(Console.YELLOW, player_char);
}
else {
io.write(player_char);
}
}
func board_draw_row(self, lc) {
let v0 = self.get(lc);
let v1 = self.get(lc + 1);
let v2 = self.get(lc + 2);
# print column 1
io.write("| "); self.player_write(v0);
# column 2
io.write(" | "); self.player_write(v1);
# column 3
io.write(" | "); self.player_write(v2);
# print end
io.write(" |\n");
print("+---+---+---+");
}
func board_draw(self) {
let index = 0;
print("+---+---+---+");
while index != 9 {
self.board_draw_row(index);
index += 3;
}
}
});
ttt.play = func (self) {
let command = "";
print("=== TICTACTOE ===");
print("Input 'q' to exit");
print("Input a number play a spot");
self.board_draw();
let int_cmd = 0;
while command != "q" {
command = Console.read();
if command == 'q' {
return 0;
}
int_cmd = command.to_int()-1;
if self.moves[int_cmd] == self.EMPTY {
self.moves[int_cmd] = self.X_SPOT;
self.check_winner(self.X_SPOT);
self.ai_move();
self.check_winner(self.O_SPOT);
self.board_draw();
}
else {
print("Space already taken!");
self.board_draw();
}
}
};
Future
using our macro syntax (check the doc for more info), we have been able to prototype more of the extensibility features we’d envisioned originally. This includes building custom control structures such as a custom match or switch-like block.
With the macro feature:
macro match(expr, block) {
let cases = [];
let _ = Type.extend({
func __lt__(self, other) {
return ['<', other];
}
func __lte__(self, other) {
return ['<=', other];
}
func __gt__(self, other) {
return ['>', other];
}
func __gte__(self, other) {
return ['>=', other];
}
func __eql__(self, other) {
return ['==', other];
}
func __noteql__(self, other) {
return ['!=', other];
}
});
macro when(ary: Array, block) {
let tok: str = ary[0];
let val = ary[1];
cases.append([tok, val, block]);
}
# yield execution to the given block
block();
mixin {
for ary in cases {
let tok = ary[0][0];
let val = ary[0][1];
let blk = ary[1];
let cond: bool = false;
if tok == '<' {
cond = expr < val;
} elif tok == '<=' {
cond = expr <= val;
} elif tok == '>' {
cond = expr > val;
} elif tok == '>=' {
cond = expr >= val;
} elif tok == '==' {
cond = expr == val;
} elif tok == '!=' {
cond = expr != val;
}
if cond {
# condition satisfied, pass execution to block
blk();
}
}
}
}
# future block syntax, passed as last param like ruby
let favorite_number = 5;
match(favorite_number) {
when(_ < 5) {
print("less than 5");
}
when(_ == 5) {
print("equal to 5");
}
}
To start the PEACH repl:
What's cool about PEACH is that much of the language is built in itself, such as boolean types and even the 'Null' object! You can peer into the code by looking at the files in std/types/
. Here's what null looks like, hopefully this gives an idea of how parts of the language are implemented internally.
let Null = Type.extend({
name = 'Null'
instance = {}
func __construct__(self, val) {
# do nothing
}
});
# Null singleton instance
Null.instance = Null.extend({
func __eql__(self, other) {
return other.type() == Null;
}
func __bool__(self) {
return 0;
}
func __not__(self) {
return 1;
}
func to_str(self) {
return 'null';
}
});
let null = Null.instance; # <-- global `null` object
@ajmd17 using our macro syntax (check the doc for more info), we have been able to prototype more of the extensibility features we’d invisioned originally. This includes building custom control structures such as a custom match
or switch
-like block.
With the macro feature:
macro match(expr, block) {
let cases = [];
let _ = Type.extend({
func __lt__(self, other) {
return ['<', other];
}
func __lte__(self, other) {
return ['<=', other];
}
func __gt__(self, other) {
return ['>', other];
}
func __gte__(self, other) {
return ['>=', other];
}
func __eql__(self, other) {
return ['==', other];
}
func __noteql__(self, other) {
return ['!=', other];
}
});
macro when(ary: Array, block) {
let tok: str = ary[0];
let val = ary[1];
cases.append([tok, val, block]);
}
# yield execution to the given block
block();
mixin {
for ary in cases {
let tok = ary[0][0];
let val = ary[0][1];
let blk = ary[1];
let cond: bool = false;
if tok == '<' {
cond = expr < val;
} elif tok == '<=' {
cond = expr <= val;
} elif tok == '>' {
cond = expr > val;
} elif tok == '>=' {
cond = expr >= val;
} elif tok == '==' {
cond = expr == val;
} elif tok == '!=' {
cond = expr != val;
}
if cond {
# condition satisfied, pass execution to block
blk();
}
}
}
}
# future block syntax, passed as last param like ruby
let favorite_number = 5;
match(favorite_number) {
when(_ < 5) {
print("less than 5");
}
when(_ == 5) {
print("equal to 5");
}
}
Wow, this is awesome!
this is really amazing but also... why?
@SeamusDonahue to create a modular/customizable language, having types internally defined, etc.
We did this all during the 10 days of the jam so it's not as polished as say a year old language, but it works fairly well.
If you have any questions, comment them down here :)
Is it possible to create a function with optional/default parameters? If so, how? If not, is it possible to add it? @emd22
nice