- Pie aims to be unique yet feel familiar
- Everything is an expression (no null/unit/none type)
- Bare-bones (if it doesn't need to be keyword, then it isn't + no operators)
- Still quirky (different even if the difference is not good)
- Variables
- Closures
- Classes
- Unions
- Collections
- Loops
- Match Expressions
- Namespaces
- Scopes
- Operators
- Overloading
- Packs
- Built-in Functions
- Types
- Keywords List
- Reserved Punctuation
- Comments
- Install
- Community
- Contributors
- What was said about Pie
Define variables using assignment with an optional type:
x = 5;
y: Int = 5;
Closures have a familiar syntax (JS syntax).
A nullary closure that returns 1 when called:
() => 1;
A closure that takes two arguments and returns the first:
(x, y) => x;
Closures can be assigned to variables:
func = (x) => "yay";
Any parameter can be named in the argument list in any order:
func = (one, two) => one;
func(two=2, one=1);
func(two=1, 1);
A function can have at most one variadic parameter. The variadic parameter can be anywhere in the parameter list.
The variadic argument has to be annotated with a type with leading ellipsis ...<type>:
getLast = (all: ...Any, last) => last;
x = getLast(1, 2, 3);
x will be 3.
Use the trailing ... to expand a pack:
getFirst = (first, rest: ...Any) => first;
forward = (args: ...Any) => getFirst(args...);
A class is a block (scope) preceded by the class keyword. The block must consist of ONLY assignments:
Human: Type = class {
name: String = "";
age: Int = 0;
prettyPrint = () => {
__builtin_print(name);
__builtin_print(age);
};
};
Construct an object by calling the constructor of the class. Optionally pass initial values to the data members:
h: Human = Human("Pie", 1);
h.age = 10;
h.prettyPrint();
Unions in Pie are what other languages call "Sum Types":
U: Type = union { Int; Double; String; };
x: U = 1;
y: U = 3.14;
z: U = "Hello";
Note: The old syntax Union { Int | Double | String } is deprecated. Use the union keyword with semicolons as shown above.
Unions also work with user-defined types.
There are 2 collections in Pie
list: {type} = {expr1, expr2, expr3};
__builtin_get(list, 1);
__builtin_set(list, 2, expr4);
__builtin_set(list, 5, expr5); .: Error! Out of bound access
map: {type1: type2} = {key1: value1, key2: value2};
__builtin_get(map, key1);
__builtin_set(map, key2, value4); .: changes key2's value
__builtin_set(map, key3, value3); .: inserts a new value
First, let's explore the general syntax of the loop construct, then we'll explore the other kinds.
loop 10 => {
__builtin_print("Hi");
};
This program prints "Hi" 10 times.
We can also introduce a loop variable:
loop 10 => i {
__builtin_print(i);
};
Note that the braces can be omitted (with or without the loop variable).
Kinds of Loops:
There are 4 kinds of loops in Pie. They all utilize the loop keyword. The kind of the loop depends on the type of the loop operand
When the type of the operand is an Int
When the type of the operand is a Bool
When the type of the operand is a pack of any kind (i.e. ...Any)
When the type of the operand is an Object. The object MUST follow the Iterator Protocol which is defined as follows: For an object to be qualified as an iterator, it must define 2 methods:
hasNext(): Boolnext()
hasNext must return a boolean indicating whether the loops should continue or terminate. next yields the next value.
Match expressions can match against 3 things.
- Value
- Type
- Structure
Value Matching:
x = 1;
match x {
=1 => print("one");
=2 => print("two");
="hi" => print("some string");
};
Type Matching:
x = 1;
match x {
:String => print("str");
:Int => print("num");
:Double => print("float");
};
Structural Matching:
C = class { a = 0; b = "";};
c = C(314, C(1, "two"));
match c {
C(x: Int, ="two") => print(x);
C(y=3, :String = "something") => print(2);
C(n: Int = 314, C(=1, ="two")) => print(n);
};
The code above ends up printing 314.
Note how you can match both the value and the type at the same time. You can even give the matched value a name.
The collection of the tokens:
<name>: <type> = <value>
is called a Single.
Guards and such:
You can match against multiple patterns in a single case by using the pipe symbol | and you can guard against any case by using the ampersand &:
x = 5;
match x {
=1 | =2 | =3 => print("one two three");
a & __builtin_lt(a, 0) => print("negative");
a & __builtin_gt(a, 0) => print("positive (not 1, 2, or 3)");
};
Of course, you can have both conditions and pipes in the same case.
Like everything else in this language, Namespaces are expressions too, and they yield a value!
Declare a namespace by using the space keyword. Assign a namespace to a variable in order to name it:
my_space = space {
__builtin_print("start");
decl1: Int = 1;
ID: (Any): Any = (x) => x;
nested = space {
__builtin_print("inner");
decl2 = "Hi";
};
__builtin_print("finish");
};
Namespaces could seem like just syntactic sugar for classes, but they're not! There is a major difference which is the fact that you can run arbitrary code inside namespaces. A class may only have assignments.
To access a member of a namespace, use the "scope resolution operator", or :::
x = space { a = 1; };
__builtin_print(x::a);
Assigning a namespace to an existing namespace will consolidate the two namespaces onto the first:
ns = space {
a = 1;
b = 2;
c = 3;
};
ns = space {
a = 5;
x = 10;
y = 20;
};
__builtin_print(ns::a); .: prints 5
__builtin_print(ns::b); .: prints 2
__builtin_print(ns::x); .: prints 10
This allows you to split code that belongs to a single namespace in multiple different files and have all the declarations be in the same namespace.
Keep in mind, if you assign a namespace to another value, it loses its content:
x = space { a = 1; };
x = 5;
x = space { b = 1; };
__builtin_print(x::a); .: ERROR!
The use directive pulls all the names in from a namespace into the current namespace.
ns = space {
x = 1;
y = "hi";
z = 3.14;
};
use ns;
__builtin_print(x);
Since everything is an expression, so are scopes! They take the value of the last expression in them.
Here, x will be assigned to 3.
x = {
1;
2;
3;
};
Since scopes take the value of their last expression, scopes cannot be empty!
The following line will error:
x = { };
Pie doesn't provide any operators. One has to define their own. For that reason, any operator symbol (+, -, *, /, etc...) can be used as a variable name.
There are 5 types of operators that can be defined:
prefix:
e.g.- 1infix:
e.g.1 + 2suffix:
e.g.5 !exfix:
e.g.[ 0 ]mixfix:
e.g.if 1 then 2 else 3
Here is how to define your own operator:
prefix(!) always_one = (x) => 1;
always_one is now a prefix operator that when applied, always returns 1!
In this example, a will come out as 1.
a = always_one 5;
infix has to be assigned to a binary closure while prefix and suffix have to be assigned to a unary closure.
What goes between the parenthesis after the keyword (i.e.prefix(+)) is the precedence. You can use any operator you want and Pie will figure it out automatically. For example, operators with precedence level + have a lower precedence than operators annotated with precedence level *. You can use user-defined-operators as precedence level.
One can nudge the precedence level by attaching a + or a - after a precedence level:
infix(*) star = (a, b) => 1;
infix(* -) plus = (a, b) => 2;
Here, the plus operator has lower precedence than star due to the * - notation, which means multiplication-level lowered by one.
An operator can also have the precedence of another operator:
infix(+) add = (a, b) => __builtin_add(a, b);
infix(add) sub = (a, b) => __builtin_sub(a, b);
infix(add +) mul = (a, b) => __builtin_mul(a, b);
Operator sub has a precedence that is equal to operator add's precedence. Operator mul, on the other hand, has a higher precedence.
You can overload operators based on the parameter types:
infix(+) + = (a: Int, b: Int): Int => __builtin_add(a, b);
infix(+) + = (a: String, b: String): String => __builtin_concat(a, b);
1 + 2;
"Hi" + "Bye";
The 1 + 2 calls the first operator. "Hi" + "Bye" calls the second!
Packs in Pie are analogous to C++'s packs. One can only declare a pack as a function parameter:
func = (pack: ...Any) => __builtin_print(pack);
func(1, "Hello", 3.14);
Note that to declare a pack, the argument MUST be given a type preceded by ellipses. Packs may be empty.
Pie supports Fold Expressions, much like C++:
(pack + ...)
(... + pack)
(init + pack + ...)
init will be used as an initial value. Helps in the case where the pack is empty:
(... + pack + init)
(pack + ... + sep)
The above expressions evaluates like this:
((((arg1 + sep) + arg2) + sep) + arg3)
This can be useful if you wanted to create a CSV entry from a bunch of strings for example.
(sep + ... + pack)
The above expression evaluates right-to-left:
(arg1 + (sep + (arg2 + (arg3 + sep))))
(init + pack + ... + sep)
(sep + ... + pack + init)
import is the only keyword that is not recognized by the interpreter. Instead, it's a pre-processor directive:
in ../folder/file.pie:
x = 1;
in main.pie
import ../folder/file;
import ../folder/file;
__builtin_print(x);
The resulting file:
x = 1;
__builtin_print(x);
Note that .pie is omitted in the import directive.
Since Pie doesn't provide any operators, how does one achieve ANYTHING at all with Pie?
Pie reserves the names starting with __builtin_.
__builtin_true(legacy)__builtin_false(legacy)__builtin_input_int__builtin_input_str
__builtin_neg__builtin_not__builtin_len(accepts strings, packs, lists, and maps)__builtin_to_string__builtin_to_int__builtin_to_double__builtin_reset__builtin_eval
__builtin_add__builtin_sub__builtin_mul__builtin_div__builtin_mod__builtin_pow__builtin_gt__builtin_geq__builtin_eq__builtin_leq__builtin_lt__builtin_and__builtin_or__builtin_get
__builtin_conditional(cond, then, else)__builtin_set(container, index, item)
__builtin_str_slice(str, start, steps, end)
__builtin_print(returns the last argument)__builtin_concat
IntDoubleBoolStringAnyTypeSyntax
{type}: list type{type1: type2}: map type
(T1, T2): T3
one: (Int): Int = (x: Int): Int => 1;
If something is left un-typed, it will be given the Any type.
...TypeRead more about packs in the packs sections
Types are values in Pie. A simple use case to demonstrate this is Type Aliases:
Num = Int;
x: Num = 1;
Some types, like function types, are not valid expressions, and therefore, the parser needs to know if it's in typing context or an expression context. To denote a typing context, we prefix the type with a colon ::
Func = :(Int, Any): String;
f: Func = (x, y): String => "hi";
The Syntax type is a special type that gives a handle onto the AST node used to represent an expression.
Take this example:
infix + = (a, b) => __builtin_add(a, b);
x: Syntax = 1 + a;
x is a handle to the AST which represents the expression 1 + a.
To evaluate x, you just need to call __builtin_eval on it:
result = __builtin_eval(x);
However, evaluating x right now will error since a is not defined. All we need to do is define a before evaluating x.
a = 5;
result = __builtin_eval(x);
__builtin_print(result);
6 will be printed.
.: this is a comment
this isn't
.::
this is a comment
this is also a comment
so is this::.
this isn't
importspaceuse
prefixinfixsuffixexfixmixfix
classunionmatch
loop
truefalse
__builtin_*( ){ },....==>:;
There are 2 ways to install Pie
Binaries exist for: 1- Linux 2- Macos x86 2- Macos Apple Silicon
Check the release section for the download link
Make sure you have git, make, and a C++ compiler that supports C++23. Then run the following in the terminal:
git clone https://github.com/PiCake314/Pie
cd Pie
make
- Lexically Scoped Operators
- Remove preprocessor
- File IO
- Use Big Int instead of
int64_t - Allow variadics of Syntax type
- Add default values to function parameters
- Make
=and=>overloadable - Fix builtin reset (value-reset, reset/name-reset)
- World domination
- Move from Make to Bake
- Improve error messages (add line and column numbers)
- Add recursive operators
- Remove dependency on stdx and boost
- Compile to WASM & a web interface
- Add LLVM backend
- Eager Parameter Evaluation (mostly done, needs more work)
- add
lexandvaluenamespaces to codebase - Add REPL
- Changed error system to exceptions (will allow REPL)
- Looping....maybee (added iterators!!!);
- Fix arbitrary names typing issue
- Same for loop^^
- Casting
(maybe using(using builtins)as) - Implement separated fold expressions (like C++)
- Add fold expressions (like C++)
- Arbitrary function parameters
- Add unions
- Add
usedirective - Add an import system (modules)
- Added global namespace access syntax
- Add namespaces
- Add match expression (like scala)
- Add overloaded operators at runtime (instead of parse-time)
- Fixed infix operators parsing right to left!
- Implemented
__builtin_eqfor all values! - Add named parameters to some builtin functions
- Add variadic arguments
- Add named arguments
- Change _builtin{true|false} to true/false;
- Fix operator return type checking..?
- Add overloading operators
- Change comments to
.::and::. - Added
input_{str|int} - Add Arbitrary Operators!!!
- Add circumfix operators
- Add lazy evaluation
- Add constructor to classes..somehow
- Allow recursion..somehow
- Add classes
- Add assignment to any expression
- Add booleans
- Add closures
Add collectionsAdd iteratorsAdd loopingAdd method operators..?Cascade operator..
- "The whole language is a bug"
- "can't have your
cakePie and eat it too!"
