- Chapter 1: Philosophy
- Chapter 2: Lexical Grammer
- Chapter 3: Program Structure
- Chapter 4: Expressions
- Chapter 5: Types
- Chapter 6: Operators
Pie is an expression-only language with dynamic strong typing & user-defined operators. At its core, Pie is just a collection of features deemed "cool" by its creator, Ali Almutawa Jr.
Keywords are modifiers, in the sense that they modify the next token (or tokens) to something of a different meaning than it would be otherwise. Keywords may not be assigned to.
Pie has 12 keywords:
classexfiximportinfixloopmatchmixfixprefixspacesuffixunionuse
Punctuation are sigils and symbols (not including letters). Punctuation may not be assigned to.
Pie reserves 12 punctuation:
(){},......==>:;
Literal Values or Built-in Values are values which need not any imports. They are readily available in the language. These values include numbers, strings, built-in types, and functions with names that start with __builtin_.
These values may be assigned to.
Literal Values are summerized in the following list:
AnyBoolDoublefalseIntStringSyntaxtrueType__builtin_*- All String Literals
- All number literals
There 2 types of comments in Pie:
Any text that is between .: and either a newline character '\n' or the end of file.
Any text that is between .:: and ::..
A Pie program consists of 0 or more expressions:
program := expression*
Any number, integer or double, is a valid expression in Pie. Pie must implement big nums for its integer types
Pie booleans have 2 literals: true and false.
Anything that starts and ends with quotes (") is considered a string.
ˆ
Identifiers are names that bind to values. What you know as "variables".
Identifier names can either be proper or improper.
A name is proper if it has these three properties:
- Contain NO spaces
- Must NOT purely consist of numbers.
- Consist of the following sigils only:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&|*+~-_\'/<>[]
These identifiers are special because they can be annotated with a type, unlike improper names.
Any valid Pie expression, that is not a proper name already, is a valid improper name. This means that all the following are valid identifiers:
5"hello"func(x, y)1 + 2
These identifiers cannot be annotated with a type.
When using the infix operator =, its LHS gets assigned to the value of the RHS.
x = y will set x to the value of y.
Assignments yield their new value.
Assignments evaluate their RHS before evaluating the LHS.
This means chained assignments will work as expected:
x = y = z will assign both x and y to the value of z.
Any expression may be assigned to any other expression, which means the following assignments are all valid:
1 + 2 = "hi"func(1, 2) = true3 = 2 + 31 = 0
Assignments can optionally have a type annotation. Only proper names may be annotated:
x: Int = 1;y: String = "hi";true: Int = 5;
The following programs are ill-formed:
1: Int = 5;1: String = "hi";1 + 2: Any = "hi";"bye": Any = true;
When assigning to a variable for the first time, the assignment also declares the variable as a new variable. Any subsequent assignments are will reassign to the existing variable. However, if a subsequent assignment contained a type annotation, then that assignment will declare a new variable with a possible different type.
Example: The following program is well-formed:
x = 1;
x = 2;
x = "hi";
a: Bool = truel
a = false;
var: Int = 5;
var: String = "five";
The following program is ill-formed:
var: Int = 5;
var = "five";
Bringing scopes into it, the following program is well-formed:
x = 5;
y = "hi";
{
x = 10;
y: Any = "bye";
};
.: x is 10 here
.: y is "hi" here
Blocks allow you to run multiple expression-statements as a single expression. They evaluate the expressions in order before then yielding the last value in them.
a = {
x = 1;
func(true);
"hi";
};
a is assigned to "hi".
Note: there is no such thing as an empty scope. The expression {} is an empty list.
Functions consist of 2 main part, the parameter list, and the body.
(a, b) => 1;
This is a function that takes 2 arguments, a and b, and returns 1.
The parameter list may optionally contain type annotations for any of the parameters. The return type is also optioinal (before the fat arrow):
(i: Int, s: String, b: Bool, a: Any): Int => 0;
If a parameter or the return type is left un-annotated, its type becomes Any.
func = (a, b, c) => b;
result = func(5, c=10, a = 20);
result will be 5.
Mixing between named arguments and positional arguments is allowed. If the name of a named-argument argument is not found in the parameter list, the program is ill-formed.
A parameter pack is a parameter that swallows all the extra arguments in a function call.
To indicate that a parameter is a pack, it must be annotated with a type preceded by an ellipsis (...).
Packs are allowed to be empty!
reduce_to_one = (x: ...Any): Any => 1;
one = reduce_to_one(1, "hi", true, {});
also_one = reduce_to_one();
At most, one parameter pack may be introduced per function.
The following program is ill-formed:
(p1: ...Int, p2: ...String) => 0;
However, other regular parameters are allowed:
getFirstNum = (first: Int, rest: ...Int) => first;
getFirstNum(1); .: returns 1
getFirstNum(1, 2, 3); .: returns 1
getLastString = (pack: ...String, last: String) => last;
getLastString("hi"); .: returns "hi"
getLastString("hi", "bye", "what!"); .: returns "what!"
someOtherFunc = (a, b, pack: ...Any, z) => z;
someOtherFunc(1, 2, 3); .: returns 3
someOtherFunc(1, 2, 3, 4, 5, 6); .: returns 6
For more information about packs, check out the section 4.8 Packs
Functions in Pie will be curried if the number of arguments is less than the number of parameters.
add = (a, b) => __builtin_add(a, b);
add3 = add(3);
eight = add3(5);
Calling a function that expects arguments without any arguments will yield the same function back:
f1 = (a, b, c) => 1;
f2 = f1();
f1 and f2 are the same function.
If a parameter pack is present in the function's parameter list, then currying will only happen if the number of arguments is less than the number of non-pack parameters.
If the number of arguments is more than the index of the position of the pack in the parameter list, then the pack is consumed from the parameter list and is introduced as an empty pack inside the environment of the resulting function. Basically, as if an empty pack was passed as an implicit argument to the pack parameter.
Example:
func = (a, b, pack: ...Any, c, d) => 0;
f1 = func(1);
f2 = func(1, 2);
f3 = func(1, 2, 3);
f4 = func(1, 2, 3, 4);
f5 = func(1, 2, 3, 4, 5);
f1is(b, pack: ...Any, c, d) => 0f2is(pack: ...Any, c, d) => 0f3is(d) => 0f4is0f5is0
Variables assigned to functions could be annotated with function types:
Int2Int: (Int): Int = (x: Int): Int => 1;
String2Int: (String): Int = (s: String): Int => 1;
getBool: (): Bool = (): Bool => true;
See the types section to see what conversions are allowed.
Any variable is considered to be a nullary function which yields its own value:
x = 5;
a = x;
b = x();
Both a and b are 5.
Packs can only be introduced as parameters to functions as shown in section 4.7.2 (Parameter Packs).
Packs may be expanded into other function calls using trailing ellipsis (...).
makePack = (ps: ...Any) => ps;
func = (a, b, c) => c;
pack = makePack(1, 2, 3);
func(pack...);
The last line is the expansion. Parameters a, b, and c become 1, 2, and 3 respectively.
Note that if the ellipsis weren't present, then parameter a would become the pack containing 1, 2, 3:
func(pack, true, 3.14);
b and c become true and 3.14 respectively.
Fold expressions are expressions that operate on packs (see section 4.7.2 Parameter Packs).
A fold expression is a binary operator that operates on a pack and ellipsis. The epxression must be surrounded by parenthesis.
There are 8 kinds of fold expressions, which are the cartesian product of these types of folds:
- Unary vs Binary Fold
- Left vs Right Fold
- Normal vs Separated Fold
Unary fold expressions do not have an initial value. This means one cannot fold over an empty pack.
-
Left Folds:
func = (pack: ...Any) => (pack + ...); func(1, 2, 3, 4);The expression above unfolds to:
(((1 + 2) + 3) + 4). -
Right Folds:
func = (pack: ...Any) => (... + pack); func(1, 2, 3, 4);The expression above unfolds to:
(1 + (2 + (3 + 4))).
The following program is ill-formed because the pack is empty:
func = (pack: ...Any) => (pack + ...);
func();
Binary fold expressions have an initial value, which goes at the opposite side of the ellipsis.
-
Left Folds:
init = 10; func = (pack: ...Any) => (init + pack + ...); func(1, 2, 3, 4);The expression above unfolds to:
((((10 + 1) + 2) + 3) + 4). -
Right Folds:
init = 10; func = (pack: ...Any) => (... + pack + init); func(1, 2, 3, 4);The expression above unfolds to:
(1 + (2 + (3 + (4 + 10)))).
In the case of an empty pack, the expression yields the initial value:
inti = 10;
func = (pack: ...Any) => (init + pack + ...);
x = func();
x is 10.
Separated fold expressions add the ability to add seperators between each operation. The separator goes on the opposite side of the pack. Spearated fold expressions, too, have a left-right, unary-binary counterpart.
-
Unary Left Folds:
seperator = 5; func = (pack: ...Any) => (pack + ... + seperator); func(1, 2, 3, 4);The expression above unfolds to:
(((((((1 + 5) + 2) + 5) + 3) + 5) + 4). -
Unary Right Folds:
seperator = 5; func = (pack: ...Any) => (seperator + ... + pack); func(1, 2, 3, 4);The expression above unfolds to:
(1 + (5 + (2 + (5 + (3 + (4 + 5)))))).
Separated unary folds also don't support empty packs.
-
Binary Left Folds:
init = 10; seperator = 5; func = (pack: ...Any) => (init + pack + ... + seperator); func(1, 2, 3, 4);The expression above unfolds to:
((((((((10 + 1) + 5) + 2) + 5) + 3) + 5) + 4). -
Unary Right Folds:
init = 10; seperator = 5; func = (pack: ...Any) => (seperator + ... + pack + init); func(1, 2, 3, 4);The expression above unfolds to:
(1 + (5 + (2 + (5 + (3 + (5 + (4 + (5 + 10)))))))).
Just like normal binary fold expression, if the pack is empty, the expression yields the initial value
Classes are first class citizens in Pie:
Human = class {
name: String = "";
age = 0;
setAge = (a: Int) => age = a;
};
The class must consist of assignment expressions only.
Data members that are assigned to functions, AKA methods, have access to a special variable with the name self. This variable is a reference to the object itself. Anything that can be done to an object, can also be done with a self reference.
Every class is provided with a constructor that initializes the data members in the order they appear in the class.
If a class member is initialized through the constructor, its initializer expression is never executed.
If no value is provided for a data members in the constructor call, its initializer value is used as a default.
C = class {
a = 0;
b: String = "";
c: Bool = false;
};
object1 = C();
object2 = C(10, "hi");
object3 = C(10, "hi", true);
- In
object1,ais0,bis"", andcisfalse - In
object2,ais10,bis"hi", andcisfalse - In
object3,ais10,bis"hi", andcistrue
Unions allow for a varibale to have multiple types:
U: Type = union {
Int;
Double;
String;
};
x: U = 1;
y: U = 3.14;
z: U = "Hello";
Unions also work with user-defined types.
See Chapter 5: Types.
Lists in Pie are a comma-separated-expressions wrapped with openning and closing curly braces.
{}is an empty list{1}is a list containing the integer1{true, 5, "hi"}is a list with 3 elements
Key-value pairs where the key and the value are separated by a colon (:) and the pairs are separated by commas (,).
{:}is an empty map{"one": 1}map with key"one"mapped to value1{1: 2, true: "yes"}map containing 2 pairs
name: type = expr
(x: type) => body
(): type => body
A typing context is anywhere in the program that an expression is expected, but a type is needed. To enter a typing context, prefix the type with a colon (:). This tells the parser to expect a type rather than an expression.
For example, the following program is ill-formed:
Int2Bool = (Int): Bool;
It can be fixed using the typing-context-operator:
Int2Bool = :(Int): Bool;
func: Int2Bool = (x: Int): Bool => true;
Typing context can also be entered even if there isn't a neeed to. The following program is well-formed:
i1 = Int;
i2 = :Int;
Any: can represent any valueBool: represents onlytrueandfalseDoubleIntStringSyntaxType
{type}: list type{type1: type2}: map type
Examples:
-
(Int, Double): StringA function that takes anIntand aDoubleand returns aString. -
(): AnyA function that takes no arguments and returnsAny.
Any type that is preceeded with ellipsis:
...type
See 4.9 Classes.
Pie is structurally typed, which means it follows a structural heirarchy.
ClassA is considered a subtype of ClassB if and only if all the members of ClassB exist in ClassA with the same type.
Example:
Human: Type = class {
name: String = "";
age = 0;
};
Named: Type = class {
name: String = "default";
};
h = Human("Pie", 2);
n: Named = h;
n.name = "Cake"; .: changes h.name too
See 4.10 Unions.
Values can be used as types in Pie. A varaible with a given type that is a value will only be able to be assigned to that value itself:
x: 1 = 1;
y: "hi" = "hi";
z: true = true;
This, on its own, is not useful. However, paired with unions, it can be very powerful:
OneToThree: Type = union { 1; 2; 3; };
x: OneToThree = 1;
x = 2;
x = 3;
Concepts are unary predicate functions which are used as types. The value assigned to a variable with such type is checked by the unary function in order to type check.
This program is ill-formed:
MoreThan10 = (x) => __builtin_gt(x, 10);
a: MoreThan10 = 5;
This program is well-formed:
MoreThan10 = (x) => __builtin_gt(x, 10);
a: MoreThan10 = 15;
Concepts also allow for what's known as "Design by Contract" where pre-conditions are the types of the arguments, and the post-condition is the return type.
Implicit Conversion is the event of assigning a value of some type to a variable declared with some other type without explicit casting.
A type is said to be converitble to to another type if it can be implicitly convertible to the other type.
Only the following conversions are allowed to happen implicitly.
- Any type ->
Any - Any type ->
Syntax - Any type
T->union { ...; T; ...; }
A function type F1 is convertible to another function type F2 if and only if:
- The number of parameters of
F1andF2are equal. - All the paramater types of
F2are convertible to the parameter types ofF1respectively. - The return type of
F1is convertible to the return type ofF2.
Note that the type of the value is used instead of the type of the variable when doing these conversion checks.
The following program is well-formed.
Number = union { Int; Double; };
x: Number = 5;
a: Int = x;
Even though the union type is not convertible to Int, the program is valid because the type of the value of x is Int.
Since types are valid expressions, type aliases are as easy as declaring a new variable:
Integer = Int;
f64 = Double;
x: Integer = 42;
y: f64 = 3.14;
There are 4 main parts to declaring a new operator.
<kind> <(precedence)>? <operator_name> = <closure_literal>
- Operator Kind
- Precedence Level
- Operator Name
- Closure Literal
example:
infix + = (a: Int, b: Int): Int = __builtin_add(a, b);
The kind of the operator based on how it should be be parsed. There are 5 kinds:
prefix:++ x:++is the prefix operator! true:!is the prefix operator
infix:1 + 2:+is the infix operator1 * 2:*is the infix operator
suffix:c ++:++is the suffix operator
exfix[ x ]: The surrounding[ ]is the operator.
mixfixif cond then x else y:if,then, andelseare the operator.cond,x, andyare the arguments.
Precedence dictates the order the parser should parse the operator in a compound expression.
Pie understand the precendence levels of these operators, which are ordered from the lowest to hight level:
=||&&|^&==,!=<=,<,>,>=<=><<,>>+,-*,/,%!,~[]()::
In addition to those, there are 2 more special precedence levels: HIGH, and LOW. These 2 special values can also be used as nudged precedences.
Note that any user-defined-operators may also be used as a precedence level.
Precedence levels may be used as-is, or nudged. Nudging can be used to allow for more precise control over the precedence of an operator.
To nudge a precedence level, use a single + or - sign:
infix(+ ) plus = (a, b) => __builtin_add(a, b);
infix(plus +) times = (a, b) => __builtin_mul(a, b);
x = 1 plus 2 times 3;
x will be 7 since operator times has a precedence level that is higher than operator plus.
Note than a higher precedence level will always be higher than a lower precedence level no matter how many times it gets nudged down:
infix(* -) L1 = (a, b) => 1;
infix(L1 -) L2 = (a, b) => 2;
infix(L2 -) L3 = (a, b) => 3;
...
infix(L9 -) L10 = (a, b) => 10;
infix(+) p = (a, b) => 0;
x = 1 L10 2 p 3;
x will be 0 because p has a lower precedence even though L10 has been nudged down from * 10 times!
In cases where the operator matches a precedence level, the precedence may be omitted:
infix + = (a, b) => __builtin_add(a, b);
infix * = (a, b) => __builtin_mul(a, b);
x = 1 + 2 * 3;
Note that exfix operators don't take any precedence level.
Any proper name is a valid name for an operator. See section 4.4.1 Proper Names
exfix operators are special because they have 2 names. To differentiate between the first name and the second name, a colon (:) is inserted between them:
exfix op1 : op2 = (a) => __builtin_print(a);
op1 "hello" op2;
The string "hello" should be printed.
mixfix operators are special since they could contain more than 1 name.
- A space is required between each name
- Insert a comma where an argument would go.
mixfix(LOW +) please print : to the terminal = (a) => __builtin_print(a);
mixfix(LOW +) this operator takes no arguments = () => __builtin_print("wow");
mixfix(LOW +) add : and : and maybe : too = (a, b, c) => __builtin_add(a, __builtin_add(b, c));
please print "hi" to the terminal;
this operator takes no arguments;
x = add 1 and 2 and maybe 3 too;
__builtin_print(x);
The following should be printed:
hi
wow
6
The operators MUST be assigned to closure literals. The arity of the closure depends on the kind of the operator:
prefix: Unary closureinfix: Binary closuresuffix: Unary closureexfix: Unary closuremixfix: n-ary closure whennequals the number of colons (:) within the operator name.
Operators with the same name be overloaded based on the types of the parameters:
infix + = (a: Int, Int): Int => __builtin_add(a, b);
infix + = (a: String, String): String => __builtin_concat(a, b);
x = 1 + 2;
s = "Pie" + " is cool";
x will be 3, and s will be "Pie is cool".
...