kscript docs
- Philosophy: Design decisions, and high-level overview of how everything works
- Octalogue: Design goals and rules of thumb
- Patterns: Duck-typing concept
- Magic Attributes: Duck-typing concept
- Operator Overloading: Feature allowing custom types to define operator semantics
- Templates: Feature on specific types
- Octalogue: Design goals and rules of thumb
- Builtins: Types, functions, and values available everywhere
- Types: Builtin types
- Functions: Builtin functions
- Exceptions: Builtin error types
- Types: Builtin types
- Modules: Standard library of kscript
- os: Operating System: Operating System Module
- io: Input/Output: Input/Output Module
- net: Networking: Networking Module
- net.http: HTTP Networking: HTTP-specific networking submodule
- net.http: HTTP Networking: HTTP-specific networking submodule
- time: Time: Time Module
- util: Utilities: Utilities Module
- gram: Grammar: Grammar Module
- m: Math: Math Module
- nx: NumeriX: NumeriX Module
- nx.la: Linear Algebra: Linear Algebra Submodule
- nx.fft: FFT: FFT Submodules
- nx.la: Linear Algebra: Linear Algebra Submodule
- ffi: Foreign Functions: Foreign Functions
- os: Operating System: Operating System Module
- Syntax: Language specification and grammar rules
- EBNF: Formal specification
- Expressions: Syntax elements yielding a result
- Integer Literal: Whole number literal syntax
- Float Literal: Real number literal syntax
- Complex Literal: Complex literal syntax
- String Literal: String literal syntax
- List Literal: List literal syntax
- Tuple Literal: Tuple literal syntax
- Set Literal: Set literal syntax
- Dict Literal: Dict literal syntax
- Lambda Expression: Lambda expression syntax
- Function Definition: Function definition syntax
- Type Definition: Type definition syntax
- Operators: Binary and unary operators
- Integer Literal: Whole number literal syntax
- Statements: Syntax elements which do not yield a value
- Expression Statement: Executes an expression
- Assert Statement: Validating an assertion
- Cont Statement: Continue through a loop
- Break Statement: Break a loop
- Ret Statement: Returning a value from a function
- Throw Statement: Throwing an exception up the call stack
- If Statement: Conditional code execution
- While Statement: Conditional loop execution
- For Statement: Iterable loop execution
- Try Statement: Exception handling construct
- Import Statement: Module importing
- Expression Statement: Executes an expression
- EBNF: Formal specification
Welcome to the kscript docs, available online at docs.kscript.org. This document was compiled on 2021-02-26
kscript is a dynamic programming language with expressive syntax, cross-platform support, and a rich standard library. kscript was designed to be a tool useful in many different circumstances – as a computer calculator, as a task-automation language, GUI development language, numerical programming language, and more.
- What is kscript?
kscript is a dynamic programming language with expressive syntax, cross platform support, and a rich standard library. Its primary aim is to allow developers to write platform agnostic programs that can run anywhere, and require little or no platform- or os- specific code.
You're currently reading the docs, which is a formal specification of the language, builtins, standard library, and semantics.
- Why is kscript?
- kscript was designed to be a tool useful in many different circumstances – as a computer calculator, as a task-automation language, GUI development language, numerical programming language, and more. A few languages may come to mind – namely Python, tcl, and so forth.
I found that I had issues with some choices the Python team made. Syntactically, I dislike required/syntactically-significant whitespace, and dislike Python’s overuse of
:
. . I feel that many Python modules (for example, the operating system module) do not give the best interface to their functionality, and often require the programmer to use platform-specific code. I’d like to conclude this paragraph with a redeeming note – Python has been very successful and I largely do enjoy the language (even though I have my complaints), and even borrow the great ideas Python has had. - Who is kscript?
- kscript is developed by free software enthusiasts, under the organization ChemicalDevelopment. Feel free to contact current authors with questions, comments, or concerns at any time:
1. Philosophy
- Octalogue: Design goals and rules of thumb
- Patterns: Duck-typing concept
- Magic Attributes: Duck-typing concept
- Operator Overloading: Feature allowing custom types to define operator semantics
- Templates: Feature on specific types
This section contains the design philosophy of kscript. kscript is meant to be very close to psuedocode, which makes reading and writing quite easy (for humans).
1.1. Octalogue
- 0. Thou Shalt Have No Languages Before KSCRIPT
- 1. Thou Shalt Not Take The Name Of KSCRIPT In Vain
- 2. Thou Shalt Share
- 3. Honour They Fauther And Thy Mother (Technology)
- 4. Thou Shalt Not Write Incorrect Code
- 5. Thou Shalt Not Write Confusing Code
- 6. Thou Shalt Not Repeat Thyself
- 7. Thou Shalt Apply Judgement
These are the pillars of kscript, which the kscript standard library and all other kscript code should try to adhere to.
- 0. Thou Shalt Have No Languages Before KSCRIPT
- When writing a package, or any code in kscript, do not prioritize other languages. Just because you are wrapping a C library does not mean other developers which use your code should feel like they're writing C code.
Any abstractions should be and should encourage well written and conventional kscript code; anything else adds to the mental strain present in that language to the already-existing mental strain of solving whatever problem they are using your code for, so don't make it harder on them.
- 1. Thou Shalt Not Take The Name Of KSCRIPT In Vain
- Don't use kscript to do bad things to people; kscript has no place being used for racist, mysogynistic, anti-LGBT+, or any other evil purposes. This should be a bare minimum of anyone doing anything
kscript was founded and written using the principles of free software, and we ask that you do the same for others as well. That way, everyone wins.
It is well known that free and open source software is more secure, encourages contributions, and is more robust. There is typically little benefit to restricting access to source code nowadays, as it doesn't even make sense financially (it makes more sense to monetize products in other ways). Regardless, we still take a stand that free and open source software is better.
- 3. Honour They Fauther And Thy Mother (Technology)
- While kscript may seek to solve problems created by languages and technologies that came before it, we must also realize that a great deal of work has been do
ne in these technologies. It would be foolish to ignore the lessons learned, and block ourselves out from the established programming languages, libraries, an
d operating systems.
Therefore, kscript developers, package developers, and users should make code work and be interoperable, when possible, with other technology solutions.
- 4. Thou Shalt Not Write Incorrect Code
- Writing code which does not work, or does not work reliably (depending on an implementation is the same as depending on the wind -- don't do it!) is a mortal sin in kscript. kscript should give you the tools to make everything cross-platform and generic. If it doesn't, then it's a problem with kscript and you should contact the developers immediately. Otherwise, it's on you!
Writing OS-specific code is incorrect cases (in most cases)
Writing code which requires quirks of a specific architecture is incorrect code (in most cases)
- 5. Thou Shalt Not Write Confusing Code
- The highest function of code is to solve problems. Although you may complete a task with small, undocumented, and poorly written code, you create more problems than you solve. By assuming things about input to a function (for example), you may simplify your task, but the number of problems does not go down; quite the opposite in fact.
When writing code, you should be good to yourself, as well as those who will end up using your code. This involves giving classes, modules, functions, and even variables accurate names.
This also includes formatting your code (possibly with an IDE or linter). Whereas some languages beat their users down with a stick named 'relevant whitespace', kscript believes that developers are not children, although they may do childish things. By all accounts, you SHOULD indent your code regularly, with consistent use of either spaces or tabs (preferably 4 spaces). But kscript isn't going to stop you from doing otherwise.
- 6. Thou Shalt Not Repeat Thyself
- Your code should never repeat itself. Your code should adhere to the DRY principle.
This includes at a micro level:
# BAD if A { B = 2 * 3 C = A + B } else { B = 2 * 3 C = other / B } # GOOD. KSCRIPT IS PLEASED B = 2 * 3 if A { C = A + B } else { C = other / B }
As well as at a macro level; The following code should never be written:
func my_min(*args) { if !args, ret 0 _m = args[0] for i in args[1:] { if i < _m, _m = i } ret i }
This algorithm has already been implemented as the built-in function min. Therefore, in kscript, we program according to the acronym DRY (Don't Repeat Yourself), but also add another: STPO (Solve The Problem Once). The idea is that macro-problems (such as: 'how to compute the minimum element in a collection?', 'what is the best way to have a logging library?', 'what is the best way to store large arrays?', etc.) should not be solved over-and-over (unless, of course, a better solution becomes apparant), but rather, solved once so that solution may be re-used by everyone.
The goal is not to have many implementations to choose from, but rather to have one implementation that is a clear choice (i.e. that you would be a fool to NOT choose it).
- 7. Thou Shalt Apply Judgement
- All of these rules are just suggestions (albeit strong, and well reasoned ones). There is, inevitably, some use case that comes along that requires the breaking of these sacred pacts.
People that have strong beliefs are often hypocritical. Just refer to C1, and don't be as bad as those links.
1.2. Patterns
Patterns in kscript refer to sets of magic attributes and/or methods that objects are expected to have in order to fulfill a certain purpose. Objects that have those attributes and/or methods are said to "fit" a given pattern, and can be used like other objects that fit that pattern -- this is the basis of all duck-typed programming languages.
Patterns are a similar concept to what are called interfaces or contracts in other language, but in practice are much more dynamic, as the developer doesn't need to specify the pattern that the object fits. You can think of interfaces/contracts as a more formal specification, whereas patterns are dynamic and only require that a given attribute/method is available when a function expects it to be. If the attribute/method is unavailable, the function processing it will likely throw an error explaining that it does not have the expected attribute and/or method.
Although the type does not matter for a pattern (objects of any type may fit a pattern), many patterns will have an abstract base type which other types are subtypes of. This is primarily done to simplify code and reduce redundancy (i.e. if all subtypes re-implemented the pattern handling code, then there would be code bloat and duplication). However, this is not required and is only done to simplify the implementation in most cases (or, for some sense of type-hierarchy purity).
Some examples of patterns in the standard library are:
- Number pattern, documented by the abstract base type number
- IO pattern, documented by the abstract base type io.BaseIO
1.3. Magic Attributes
Magic attributes are the name we use to describe how the standard library (or external packages) inspect objects to determine how to use them for a particular purpose.
For example, when int(x)
is called, and x
is an unfamiliar type (for example, a custom type), how does kscript know how to convert it to an integer? Well, for converting to integers, there is a well established magic attribute called __int, which is searched for on x
(for example, x.__int()
is attempted). If that does not work, there is a secondary magic attributed called __integral, which is then searched (x.__integral()
is attempted). If both of those fail, then a TypeError is thrown with a message explaining that x
could not be converted to an int. However, if one of those did succeed, then its return value is expected to be an integer, and kscript can use it as the return value.
The above was just an example, but it shows how specially named attributes allow for different libraries and programs to communicate and translate objects into known quantities for processing. This section contains examples and commonly used magic attributes that you can use in your own code to write easier-to-use libraries and programs.
- __int()
Used for direct conversion to int. Should return an int. Also see __integral
- __integral()
- Used for integral value conversion. Sshould return an int. Also see __int
- __bool()
- Used for conversion to bool, Should return a bool
- __bytes()
- Used for direct conversion to bytes. Should return a bytes
- __str()
- Used for direct conversion to str. Should return a str
- __repr()
- Used for string representation (for example, the repr function). Should return an str
- __hash()
- Used for computing a hash of an object. Should return an int
- __len()
- Used for computing the length of a container, which is typically the number of elements. Should return an int
1.4. Operator Overloading
There are a number of operators in kscript that can be used on builtin types and types in the standard library. However, you can also use them with custom types, which is covered in this section. Defining semantics for operators is called operator overloading.
Operator overloading in kscript is done via magic attributes. Specifically, there are a few different cases:
- For unary operators, such as
+x
, the magic attribute (in this case,__pos
for+
) is searched ontype(x)
. So,type(x).__pos(x)
is attmpted. If no such attribute existed, an Error is thrown - For unary operators, such as
x+y
, the magic attribute (in this case,__add
for+
) is searched ontype(x)
andtype(y)
. So,type(x).__add(x, y)
is attmpted. If no such attribute existed, or the result wasundefined
, thentype(y).__add(x, y)
is attmpted. If no such attribute existed, or the result wasundefined
, then an Error is thrown
The magic attributes for various operators are listed below
Unary operators:
Binary operators:
+
: __add-
: __sub*
: __mul@
: __matmul/
: __div//
: __floordiv%
: __mod**
: __pow<<
: __lsh>>
: __rsh|
: __binior^
: __binxor&
: __binand==
: __eq!=
: __ne<
: __lt<=
: __le>
: __gt>=
: __ge
Here's a short example showing how to overload the +
operator:
# Example class which wraps a value (.val)
type Foo {
func __init(self, val) {
self.val = val
}
# Adds two 'Foo' objects (or any objects with a '.val')
func __add(L, R) {
# Create a new 'Foo' object with the added values
ret Foo(L.val + R.val)
}
}
# Create two objects
A = Foo(1)
B = Foo(2)
# Prints '3'
print ((A + B).val)
1.5. Templates
Templates are a way to create subtypes of a given type based on parameters (called "template parameters"). This is done by subscripting an existing type with []
. Some examples of templates in the standard library are:
- OSError, which can be templated on the
errno
value returned by the OS (for example,OSError[3]
) - ffi.ptr (
ffi.ptr[T]
), which can be templated on the FFI type which it points to
2. Builtins
- Types: Builtin types
- Functions: Builtin functions
- Exceptions: Builtin error types
Builtins in kscript are the types, functions, and values that are available in the global namespace and thus available everywhere without the use of import
statements. They are the most fundamental objects in kscript and are used most frequently, and so they are also made easiest to remember and type. Although kscript is duck-typed, using the builtin types is often recommended, as they have good performance and easy-to-use APIs.
Since kscript is a purely object-oriented language, everything in kscript is an instance of the object type, either directly or indirectly. This means that even types are objects, and can be treated generically as such. So can functions. This is in steep contrast to more static programming languages like C, C++, Rust, and so forth, which allow some limited compile time reflection, but little to no runtime inspection of types. kscript, however, is completely dynamic and allows things that other programming languages just can't offer. Here are a few examples:
- Container types (list, tuple, dict, ...) can store objects of any types, and multiple objects of different types
- Iterable types may yield objects of any types, and multiple objects of different types
- Objects which are not func instances may also be callable, if they implement the __call attribute
2.1. Types
- object()
- type(obj)
- number(obj=0)
- int(obj=0, base=none)
- bool(obj=false)
- float(obj=0.0, base=none)
- complex(obj=0.0+0.0i)
- str(obj='')
- bytes(obj='')
- regex(expr)
- list(objs=[])
- tuple(objs=())
- set(objs=none)
- dict(objs=)
- object()
The most generic type, which is the common type that all other types are derived from. The code
isinst(x, object)
always returns true, no matter what objectx
refers to.Objects can be created via the constructor,
object()
, which returns a blank objects that has writeable and readable attributes- type(obj)
- This type is what other types are an instance of.
type
is also an instance oftype
.Unlike most other types, the code
type(x)
does not created a new type; rather, it returns the type ofx
- number(obj=0)
- This type is the abstract base type of all other builtin numeric types (int, bool, float, complex).
By default,
number()
will return the integer0
. You can pass it any numeric type and it will return one of the builtin numeric types (or throw an error if there was a problem). You can also call it with a string, and it will parse the string and return a builtin numeric type.This type also defines and implements stubs for the "number pattern", which dictates how numeric types should behave. Specifically, an object
obj
is said to follow the number pattern if at least one of the following magic attributes holds:- If
obj.__integral()
exists,obj
is assumed to be an integer, and this method should return an int with an equivalent numerical value - If
obj.__float()
exists,obj
is assumed to be a real floating point number, and this method should return a float with an equivalent numerical value - If
obj.__complex()
exists,obj
is assumed to be a complex floating point number, and this method should return a complex with an equivalent numerical value
Operations between numbers use type coercion, with the following rules (unless otherwise stated):
- If all operands are integers, then an integer result is returned
- If all operands are either integers or real floating point numbers, then a real floating point result is returned
- If none of the above are applicable, then at least one operand must be a complex floating point number, and a complex floating point result is returned
- If
- int(obj=0, base=none)
- This type describes integers (i.e. whole numbers). This type is a subtype of number, and subscribes to the number pattern. You can create integers with integer literals, or through using this type as a constructor. If
obj
is a string, thenbase
can also be given, which is an integer describing the base format that the string is in.See: Integer Literal for creating literals
Some languages (C, C++, Java, and so forth) have sized integer types which are limited to machine precisions (see: here). For example, a 32 bit unsigned integer may only represent the values from $0$ to $2^{32}-1$. However, in kscript, all integers are of arbitrary precision – which means they may store whatever values can fit in the main memory of your computer (which is an extremely large limit on modern computers, and you are unlikely to hit that limit in any practical application).
- bool(obj=false)
- This type describes booleans. There are two boolean values,
true
andfalse
. Sometimes, these are referred to as0
/1
,on
/off
, or evenyes
/no
.true
andfalse
are keywords which result in these values.You can convert an object to a boolean (its "truthiness", or "truth" value) via this type as a function. For example,
bool(x)
turnsx
into its truthiness value, or, equivalently,x as bool
. Typically, types that have custom truthiness logic work as expected -- numbers convert totrue
if they are non-zero, containers convert totrue
if they are non-empty, and so on. In general, ifbool(x) == true
, thenx
is non-empty, non-zero, and/or valid. Otherwise, it is empty, zero, or perhaps invalid. Specific types may overload it in a way that makes sense for them, so always beware of types that override this functionality.This conversion to bool is dictated by the __bool magic attribute.
Examples:
>>> bool() false >>> bool(false) false >>> bool(true) true >>> bool(0) false >>> bool(1) true >>> bool(255) true >>> bool('') false >>> bool('AnyText') true
- float(obj=0.0, base=none)
- This type describes floating point numbers. This type is a subtype of number, and subscribes to the number pattern. You can create floats with float literals, or through using this type as a constructor. If
obj
is a string, thenbase
can also be given, which is an integer describing the base format that the string is in.See: Float Literal for creating literals
In addition to real numbers, this type can also represent infinity (positive and negative) (via
inf
and-inf
), as well as not-a-number (vianan
) values.- float.EPS
- The difference between 1.0 and the next largest number
>>> float.EPS 2.22044604925031e-16
- float.MIN
- The minimum positive value representable as a float
>>> float.MIN 2.2250738585072e-308
- float.MAX
- The maximum positive (finite) value representable as a float
>>> float.MAX 1.79769313486232e+308
- float.DIG
- The number of significant digits that can be stored in a float
>>> float.DIG 15
- complex(obj=0.0+0.0i)
- This type describes a complex number with a real and imaginary components (which can take on any float values). You can access those elements via the
.re
and.im
attributes, which will result in float objects.See: Complex Literal for creating literals
- .re
- Real component of a complex number, as a float
>>> (2 + 3i).re 2.0 >>> (3i).re 0.0 >>> (2 + 0i).re 2.0
- .im
- Imaginary component of a complex number, as a float
>>> (2 + 3i).im 3.0 >>> (3i).im 3.0 >>> (2 + 0i).im 0.0
- str(obj='')
- This type describes a string, which is a sequence of 0-or-more Unicode characters. It is the basic unit of textual information in kscript, and can store any Unicode sequence. All operations are in terms of characters (also called codepoints), and not neccessarily bytes.
Some languages have a different type for a single character and a string. However, in kscript, a character is simply a string of length 1. And, the empty string is the string of length 0. Additionally, strings are immutable (which means they cannot be changed). For example,
x[0] = 'c'
will throw an error in kscript. Instead, you should use slicing and re-assign to the same name:x = 'c' + x[1:]
.You can create a string via calling this type as a function with an argument (default: empty string). For non-str objects, conversion depends on the __str magic attribute, which is expected to return a str.
See String Literal for creating literals.
Internally, kscript uses UTF-8 to store the textual information.
Strings can be added together with the
+
operator, which concatenates their contents. For example,'abc' + 'def' == 'abcdef'
- str.upper(self)
- Computes an all-upper-case version of
self
- str.lower(self)
- Computes an all-lower-case version of
self
- str.isspace(self)
- Computes whether all characters in
self
are space characters - str.isprint(self)
- Computes whether all characters in
self
are printable characters - str.isalpha(self)
- Computes whether all characters in
self
are alphabetical characters - str.isnum(self)
- Computes whether all characters in
self
are numeric characters - str.isalnum(self)
- Computes whether all characters in
self
are alphanumeric - str.isident(self)
- Computes whether
self
is a valid kscript identifier - str.startswith(self, obj)
- Computes whether
self
starts withobj
(which may be a string, or a tuple of strings) - str.endswith(self, obj)
- Computes whether
self
ends withobj
(which may be a string, or a tuple of strings) - str.join(self, objs)
- Computes a string with
self
in between every element ofobjs
(converted to strings) - str.split(self, by)
- Returns a list of strings created when splitting
self
byby
(which may be a string of tuple of seperators) - str.index(self, sub, start=none, end=none)
- Find a substring within
self[self:end]
, or throw an error if it was not found - str.find(self, sub, start=none, end=none)
- Find a substring within
self[self:end]
, or return-1
if it was not found - str.replace(self, sub, by)
- Returns a string with instances of
sub
replaced withby
- str.trim(self)
- Trims the beginning and end of
self
of spaces, and returns what is left
- bytes(obj='')
- This type represents a bytestring, which is similar to str. The main difference being that str is a sequence of Unicode characters, whereas bytes is a sequence of bytes -- which may or may not be textual data.
You can create a bytestring by calling this type as a function with an argument (default: empty bytestring). For non-bytes objects, conversion depends on the __bytes magic attribute. Commonly, calling
bytes(x)
wherex
is a str will result in the UTF-8 bytes of that string. - regex(expr)
- This type represents a regular expression (regex). Objects of this type can be used to search through text, or even tokenize streams (see gram.Lexer).
Although in some languages or libraries, the term "regex" refers to extended regular expressions (containing backreferences, recursive patterns, and so forth), the kscript regex type is a true regex in that it describes exactly the regular languages. Though restrictive, it ensures that the implementation can be fast and efficient, and so that generative code and inspection of regex patterns is viable.
See Regex Literal for creating literals.
kscript allows you to inspect regular expressions, via the util.Graph type
- regex.exact(self, src)
- Compute a boolean describing whether the string
src
matches the regexself
exactly - regex.matches(self, src)
- Compute a boolean describing whether the string
src
matches the regexself
anywhere
- list(objs=[])
- This type represents as list (sometimes called an array in other languages), are collections of elements that can be mutated (i.e. changed).
Lists can be created by either calling the constructor (
list(objs)
), or via list literals.- list.push(self, *args)
- Pushes all of
args
to the list - list.pop(self, num=none)
- This function behaves differently based on whether
num
is given- If
num
is not given, then a single object is removed from the end and returned - If
num
is given, then an iterable of lengthnum
is returned and the lastnum
elements are removed from the list
- If
- list.index(self, obj)
- Returns the (first) index of
obj
withinself
, throwing an Error ifobj
was not inself
- list.sort(self, cmpfunc=none, keys=none)
- Sorts the list, in place, according to
cmpfunc
applied tokeys
. Ifcmpfunc
is given, it is expected to be of the signature:cmpfunc(L, R)
givingL <= R
. And, ifkeys
is given it is expected to be an iterable which are used for the corresponding inputs in the list
- tuple(objs=())
- This type represents an immutable collection, similar to list, which is indexed by position.
Tuples can be created by either calling the constructor (
tuple(objs)
), or via tuple literals. - set(objs=none)
- This type represents a mutable collection of unique (by hash and equality) objects, ordered by first insertion order. This type is similar to dict, but has no values associated with each key.
If you try and add an object to a set that already contains an equivalent object, nothing is changed.
Sets can be created by either calling the constructor (
set(objs)
), or via set literals. - dict(objs=)
- This type represents a mutable mapped collection of keys and values, with unique (by hash and equality) keys. It is implemented using a hash table, and is sometimes referred to as a hash table, dictionary, key-value store, or associative array.
This type is similar to the set type, but each element has an associated value.
Dictionaries can be created by either calling the constructor (
dict(objs)
), or via dict literals.Objects of this type have their contents ordered by first unique key insertion, which is reset when a key is deleted from a dictionary. So, setting the value of a new key means it is the last entry in the dictionary, and setting the value of an already existing key does not change the order of the dictionary.
You can subscript a dictionary like
x[key]
to retreive the value for a given key (or throw aKeyError
ifkey
is not present inx
), and you can assign to a dictionary likex[key] = val
>>> x = {"Good": 1, "Neutral": 0, "Bad": -1} {'Good': 1, 'Neutral': 0, 'Bad': -1} >>> len(x) 3 >>> x['Good'] 1 >>> x['Other'] KeyError: 'Other' Call Stack: #0: In '<inter-5>' (line 1, col 1): x['Other'] ^~~~~~~~~~ In <thread 'main'>
2.2. Functions
- print(*args)
- printf(fmt, *args)
- ord(chr)
- chr(ord)
- issub(tp, of)
- isinst(obj, of)
- repr(obj)
- bin(obj)
- oct(obj)
- hex(obj)
- abs(obj)
- hash(obj)
- len(obj)
- id(obj)
- open(src, mode='r')
- input(prompt='')
- exit(code=0)
- iter(obj)
- next(obj)
- print(*args)
Prints all of
args
to the standard output (os.stdout), by converting each one to a string, and adding a space between each argument. After all elements are printed, a newline is also printed.For finer grained control of output format see the printf function
- printf(fmt, *args)
- Prints all of
args
, formatted withfmt
. No newline or spaces are added between arguments or after all of them.The format string,
fmt
, is expected to be made up of format specifiers and normal printable text. A format specifier is started with the character%
(percent sign), and followed by fields, which control how the object is converted. Finally, each format specifier ends with a single character denoting the type. Text in between format specifiers is output verbatim without modificationFlags are optional characters that change the formatting for a specifier. All flags for a format specifier should be placed immediately after the %. Although different types may treat flags differently, generally their behavior is:
+
causes the sign of numeric outputs to always be included (so, even positive numbers will have their sign before the digits)-
causes the output to be left-aligned instead of right-aligned0
causes the output of right-aligned numbers to contain leading 0s instead of spaces
After flags, there is an optional width field which can be an integer (for example,
%10s
has a width of10
), or a*
, which takes the next object from args, treats it like an integer, and treats that as the width (dynamic width).After width, there is an optional precision field which can be a
.
followed by an integer (for example%.10s
has a precision of10
), or.*
, which takes the next object from args, treats it like an integer, and treats that as the precision (dynamic precision).Finally, there is the single-character format specifier which tells the type of output. Below are a table of specifiers:
- %%
- A literal
%
is added, and no more objects from the arguments are consumed - %i, %d
- The next object from
args
is consumed, interpreted as an integer, and the base-10 digits are added - %b
- The next object from
args
is consumed, interpreted as an integer, and the base-2 digits are added - %o
- The next object from
args
is consumed, interpreted as an integer, and the base-8 digits are added - %x
- The next object from
args
is consumed, interpreted as an integer, and the base-16 digits are added - %f
- The next object from
args
is consumed, interpreted as a floating point number, and the base-10 digits are added
Examples:
>>> printf('|%i|', 123) # Direct translation |123| >>> printf('|%5i|', 123) # Pads to size 5 |123 | >>> printf('|%+5i|', 123) # Pads to size 5, and always includes sign |+123 | >>> printf('|%-5i|', 123) # Pads to size 5, and is left-aligned | 123| >>> printf('|%-+5i|', 123) # Pads to size 5, and always includes sign, and is left-aligned | +123| >>> printf('|%05i|', 123) # Pads to size 5, and includes leading zeros |00123| >>> printf('|%0*i|', 5, 123) # Equivalent, but takes the width argument before the value it prints |00123|
For formatted output on arbitrary IO objects, see the io.BaseIO.printf function.
- ord(chr)
- Converts a character (
chr
), which should be a length-1 string, into its Unicode codepoint and return it as an integerExamples:
>>> ord('a') 97 >>> ord('b') 98
This function is the inverse of chr
- chr(ord)
- Converts an ordinal (
ord
), which is an integer, into a character. Assumesord
is the integer codepoint in Unicode.Examples:
>>> chr(0x61) 'a' >>> chr(0x62) 'b'
This function is the inverse of ord
- issub(tp, of)
- Calculates whether
tp
is a subtype (or is the same type) asof
, which should be either a type, or a tuple of types.Examples:
>>> issub(int, object) true >>> issub(int, number) true >>> issub(number, int) false
See also: isinst
- isinst(obj, of)
Calculates whether
obj
is an instance ofof
or a derived type.of
can be atype
or a tuple of types. Equivalent toissub(type(obj), of)
.Examples:
>>> isinst(1, object) true >>> isinst(1, number) true >>> isinst(1, float) false
See also: issub
- repr(obj)
Converts an object (
obj
) into a string representation, which delegates totype(obj).__repr(obj)
(see __repr), and must result in a string.In general, this function returns a string of kscript code which will result in
obj
if executed. For example:repr(1) == '1'
,repr('abc') == '\'abc\''
, andrepr([1, 2, 3]) == '[1, 2, 3]'
. Although, this is not always possible. For example,repr(object()) == '<\'object\' @ 0x564460528DD0>'
. For many types, it is equivalent to converting to a str, with the notable exception ofstr
objects themselves -- which add'
and escape sequences.Examples:
>>> repr(1) '1' >>> repr("abc") '\'abc\'' >>> repr([1, 2, 3]) '[1, 2, 3]'
- bin(obj)
Return a string beginning with
0b
and containing the base-2 digits ofobj
(which should be an integer)- oct(obj)
- Return a string beginning with
0o
and containing the base-8 digits ofobj
(which should be an integer) - hex(obj)
- Return a string beginning with
0x
and containing the base-16 digits ofobj
(which should be an integer) - abs(obj)
- Computes the absolute value of an object, which delegates to
type(obj).__abs(obj)
(see __abs).For numeric types, it returns the expected values
For os.path objects, it returns the real absolute path
Examples:
>>> abs(0) 0 >>> abs(1.0) 1.0 >>> abs(-1.0) 1.0 >>> abs(1 + 2i) 2.23606797749979
- hash(obj)
- Computes the hash of an object, which delegates to
type(obj).__hash(obj)
(see __hash), and must be an integral value.Most immutable builtin types (including numeric types, tuple, str, and bytes) provide a hashing function. And by default, new types created hash to their id. However, mutable collection types (such as list, set, and dict) are not hashable and will throw an error when they are attempted to be hashed.
Examples:
>>> hash(1) 1 >>> hash("abc") 193485963 >>> hash((1, 2, 3)) 4415556888914394581 >>> hash([1, 2, 3]) TypeError: 'list' object is not hashable Call Stack: #0: In '<expr>' (line 1, col 1): hash([1, 2, 3]) ^~~~~~~~~~~~~~~ #1: In hash(obj) [cfunc] In <thread 'main'> >>> hash([1, 2, 3] as tuple) # Must wrap as a tuple 4415556888914394581
- len(obj)
Computes the length of an object, which delegates to
type(obj).__len(obj)
(see __len), and must be an integral valueFor builtin container types (list, tuple, dict, and so on), it returns the number of elements
Examples:
>>> len([]) 0 >>> len([1, 2, 3]) 3 >>> len("abcd") 4
- id(obj)
Converts an object (
obj
) into its unique ID, which is an integer that is guaranteed to not be equivalent to any two currently living objects.Most of the time, this is the memory address of the object
- open(src, mode='r')
- Opens a file on disk an returns an io.FileIO object representing the open connection. Throws an error if
src
could not be opened.If
mode
is given (default: read text), then it should be one of the following:r
: Read text (the file must exist)rb
: Read bytes (the file must exist)r+
: Read and write text (the file must exist)rb+
orr+b
: Read and write bytes (the file must exist)w
: Write text (the file is cleared)wb
: Read bytes (the file is cleared)w+
: Read and write text (the file is cleared)wb+
orw+b
: Read and write bytes (the file is cleared)a
: Read and write text (the file is appended to)ab
: Read bytes (the file is appended to)a+
: Read and write text (the file is appended to)ab+
ora+b
: Read and write bytes (the file is appended to))
- input(prompt='')
- Prints an (optional) prompt, and then return the next line of user input
- exit(code=0)
- Exits the program with an exit code (default: success)
- iter(obj)
- Converts
obj
into an iterable, via the __iter attribute. Iftype(obj).__next
already exists,obj
is returned (as it is already an iterable) - next(obj)
- Returns the next object in an iterable (which may be created via iter()). Uses the __next attribute.
If
obj
was not an iterable, it is first converted to an iterable, and then the __next attribute is searched on the iterable. For example,next([1, 2, 3])
returns1
2.3. Exceptions
- Exception(what='')
- OutOfIterException()
- Error(what='')
- InternalError(what='')
- SyntaxError(what='')
- ImportError(what='')
- TypeError(what='')
- TemplateError(what='')
- NameError(what='')
- AttrError(what='')
- KeyError(what='')
- IndexError(what='')
- ValError(what='')
- AssertError(what='')
- MathError(what='')
- ArgError(what='')
- SizeError(what='')
- OSError(what='')
Exception/Errors can be thrown via the throw
statement and caught via the try
statement. Anything thrown must be an instance (either directly or indirectly) of the Exception type.
Exceptions are more general than errors; errors indicate that something has gone wrong, whereas exceptions can be used to disrupt control flow (see OutOfIterException). Therefore, in try/catch
sometimes it is recommended to only catch Error
objects, and allow exceptions to pass upwards.
- Exception(what='')
This is the base type of all other exceptions. Only objects which are a subtype of Exception may be thrown via the
throw
statement (and thus caught via thetry/catch
construct). Unlike most patterns in kscript (see Patterns), exception throwing requires the type of the object being thrown to be a derived type of this, instead of simply having magic attributes.- OutOfIterException()
- This error is thrown whenever an iterable runs out of elements. For example, when next is called but no more elements are left
Exceptions of this type are automatically caught by
for
loops, which tell the loop to stop runningThis is a subtype of Exception
- Error(what='')
- Generic error which means something bad happened and the operation could not be completed. Generally, Exception types can be used to alter control flow and not neccessarily signal an error (for example, OutOfIterException being used by
for
loops). However, this type and its subtypes are used to indicate such an error that should cause a program to halt (unless caught)This is a subtype of Exception
- InternalError(what='')
- Errors of this type are thrown when something internally isn't working. Sometimes, it may signal a bug in kscript (such as an unexpected status, unexpected configuration, etc), or it may signal a C library returned something it promised not to.
In any case, these errors are hard to correct for, and when one is thrown, the status of the interpreter itself and objects that were mutated cannot be guaranteed to be predictable
- SyntaxError(what='')
- Errors of this type are generally thrown at parsing time when a program is invalid, which could mean a number of things (unexpected tokens, invalid constructs, invalid characters, and so forth). Generally they have a descriptive message and highlight the offending part of the code, for easy debugging.
Examples:
# Some of this code is invalid... >>> for } SyntaxError: Unexpected token for } ^ @ Line 1, Col 5 in '<expr>' Call Stack: In <thread 'main'> >>> 2def SyntaxError: Unexpected token, expected ';', newline, or EOF after 2def ^~~ @ Line 1, Col 2 in '<expr>' Call Stack: In <thread 'main'>
- ImportError(what='')
- Errors of this type are generally thrown when importing a module fails, for whatever reason, including: missing dependencies, no such module, and error during initialization.
Examples:
>>> import asdf ImportError: Failed to import 'asdf' >>> import crazy_name_that_doesnt_exist ImportError: Failed to import 'crazy_name_that_doesnt_exist'
- TypeError(what='')
- Errors of this type are thrown when the type of an object doesn't match what is expected, or the type lacks an expected attribute (i.e. does not follow a pattern).
Examples:
>>> int([]) TypeError: Could not convert 'list' object to 'int'
- TemplateError(what='')
- Errors of this type are thrown when a type is templated incorrectly, or an operation is forbidden by a template.
Examples:
>>> object[] TemplateError: 'object' cannot be templated
This type is a subtype of TypeError
- NameError(what='')
Errors of this type are thrown when a variable/function/module name is used but has not been defined
Examples:
>>> asdf NameError: Unknown name: 'asdf' Call Stack: #0: In '<expr>' (line 1, col 1): asdf ^~~~ In <thread 'main'>
- AttrError(what='')
-
Errors of this type are thrown when any of the following things occur:
- An attribute is requested on an object that doesn't have any such attribute
- An attribute is read-only, but some code attempts to change it
Examples:
>>> object().a AttrError: 'object' object had no attribute 'asdf' Call Stack: #0: In '<expr>' (line 1, col 1): object().asdf ^~~~~~~~~~~~~ In <thread 'main'>
- KeyError(what='')
-
Errors of this type are thrown when any of the following things occur:
- A key to a container (for example, a dict) is not found when searched
- A key to a container is invalid (for example, dict keys must be hash -able)
Examples:
>>> x = { 'a': 1, 'b': 3 } { 'a': 1, 'b': 3 } >>> x['c'] KeyError: 'c' Call Stack: #0: In '<inter-2>' (line 1, col 1): x['c'] ^~~~~~ In <thread 'main'>
- IndexError(what='')
-
Errors of this type are thrown when any of the following things occur:
- A index to a sequence (for example, a list) is out of range
Examples:
>>> x = ['a', 'b'] ['a', 'b'] >>> x[3] KeyError: Index out of range Call Stack: #0: In '<inter-5>' (line 1, col 1): x[2] ^~~~ In <thread 'main'>
This type is a subtype of the KeyError type
- ValError(what='')
-
Errors of this type are thrown when a value provided does not match what is expected
Examples:
>>> nan as int ValError: Cannot convert 'nan' to int Call Stack: #0: In '<expr>' (line 1, col 1): nan as int ^~~~~~~~~~ #1: In int.__new(self, obj=none, base=10) [cfunc] In <thread 'main'>
- AssertError(what='')
- Errors of this type are thrown when an
assert
statement has a falsey conditionalExamples:
>>> assert false AssertError: Assertion failed: 'false' Call Stack: #0: In '<inter-0>' (line 1, col 1): assert false ^~~~~~~~~~~~ In <thread 'main'>
- MathError(what='')
- Errors of this type are thrown when a mathematical operation is given invalid or out of range operands
Examples:
>>> 1 / 0 MathError: Division by 0 Call Stack: #0: In '<expr>' (line 1, col 1): 1 / 0 ^~~~~ #1: In number.__div(L, R) [cfunc] In <thread 'main'> >>> import m >>> m.sqrt(-1) MathError: Invalid argument 'x', requirement 'x >= 0' failed Call Stack: #0: In '<expr>' (line 1, col 1): m.sqrt(-1) ^~~~~~~~~~ #1: In m.sqrt(x) [cfunc] In <thread 'main'> >>> m.sqrt(-1 + 0i) # Make sure to pass a 'complex' in if you want complex output 1.0i
- ArgError(what='')
-
Errors of this type are thrown when arguments to a function do not match the expected number, or type
Examples:
>>> ord('a', 'b') ArgError: Given extra arguments, only expected 1, but given 2 Call Stack: #0: In '<expr>' (line 1, col 1): ord('a', 'b') ^~~~~~~~~~~~~ #1: In ord(chr) [cfunc]
- SizeError(what='')
-
Errors of this type are thrown when arguments are of invalid sizes/shapes
- OSError(what='')
-
Errors of this type are thrown when an error is reported by the OS, for example by setting
errno
in CThis is a templated type, which means there are subtypes based on the type of error expressed. The specific templated types are sometimes platform-specific, and we are currently working to standardize what we can.
Examples:
>>> open("NonExistantFile.txt") OSError[2]: Failed to open 'NonExistantFile.txt' (No such file or directory) Call Stack: #0: In '<expr>' (line 1, col 1): open("NonExistantFile.txt") ^~~~~~~~~~~~~~~~~~~~~~~~~~~ #1: In open(src, mode='r') [cfunc] #2: In io.FileIO.__init(self, src, mode='r') [cfunc] In <thread 'main'>
3. Modules
- os: Operating System: Operating System Module
- io: Input/Output: Input/Output Module
- net: Networking: Networking Module
- time: Time: Time Module
- util: Utilities: Utilities Module
- gram: Grammar: Grammar Module
- m: Math: Math Module
- nx: NumeriX: NumeriX Module
- ffi: Foreign Functions: Foreign Functions
This section documents the builtin modules in kscript, of which there are plenty! The general philosophy in kscript is to make APIs as cross-platform and backend-independent as possible. What that means is that standard types, functions, and modules put forth names and functionality that might not directly map to a particular existing library -- even if kscript uses that library internally to perform the functionality.
You can count on these modules being available in any kscript distribution, and having a reliable API. Unreliable/non-standard functions, types, and variables typically begin with an underscore (_
), so be weary if using one of those functions, it might not be available everywhere!
A good example of this is the os module, which uses the C standard library to perform tasks, but the functions will have different and sometimes better suited names to what they actually do.
You can access modules by using the import
statement. For example, import os
will import the os
module, and allow you to use os.<funcname>
3.1. os: Operating System
- os.argv
- os.stdin
- os.stdout
- os.stderr
- os.getenv(key, defa=none)
- os.setenv(key, val)
- os.cwd()
- os.chdir(path)
- os.mkdir(path, mode=0o777, parents=false)
- os.rm(path, parents=false)
- os.listdir(path)
- os.glob(path)
- os.stat(path)
- os.lstat(path)
- os.fstat(fd)
- os.pipe()
- os.dup(fd, to=-1)
- os.exec(cmd)
- os.fork()
- os.mutex()
- os.thread(of, args=(), name=none)
- os.proc(argv)
- os.path(path='.', root=none)
This module, os
, allows code to interact with the operating system (OS) that the program is running on. For example, creating directories, gathering information about the filesystem, launching threads, launching processes, and accessing environment variables are all covered in this module.
Due to differences between operating systems that kscript can run on, some functionality may be different, or even missing on some platforms. We attempt to document known cases of functions behaving differently or when something is not supported. Typically, an error is thrown whenever something is not available on a particular platform.
- os.argv
List of commandline arguments passed to the program. Typically,
os.argv[0]
is the file that was ran, andos.argv[1:]
are the arguments given afterward- os.stdin
- A readable io.FileIO object representing the input to the program
- os.stdout
- A writeable io.FileIO object representing the output from the program
- os.stderr
- A writeable io.FileIO object representing the error output from the program
- os.getenv(key, defa=none)
- Gets an environment variable corresponding to
key
(which is expected to be a string)In the case that
key
did not exist in the environment, this function's behavior depends on whetherdefa
is given.- If
defa
is given, then it is returned - Otherwise, an OSError is thrown
- If
- os.setenv(key, val)
- Sets an environment variable corresponding to
key
(which is expected to be a string) toval
(which is also expected to be a string)In the case that something went wrong (i.e. an invalid name, general OS error), an OSError is thrown
- os.cwd()
- Get the current working directory of the process, as an os.path object
To retrieve it as a string, the code
os.getcwd() as str
can be usedTo change the working directory, see the os.chdir function
- os.chdir(path)
- Set the current working directory of the process to
path
.path
is expected to be either a string or an os.path objectIf
path
did not exist, or for some other reason the operation failed (i.e. permissions), an OSError is thrown - os.mkdir(path, mode=0o777, parents=false)
- Creates a directory on the filesystem at
path
, with modemode
. Mode is expected to be a octal numerical notation of the file permission bits (default: allow everything for everybody)If
parents
is truthy, then parents ofpath
that do not exist are created recursively. Ifparents
is false, then an error is raised when trying to create a directory within another directory that does not exist yet - os.rm(path, parents=false)
- Removes a file or directory
path
from the filesystemIf
path
refers to a directory, then the behavior depends onparents
:- If
parents
is truthy, then non-empty directories have their contents recursively deleted - Otherwise,
path
will only be removed if it is empty; otherwise this function will throw an OSError
- If
- os.listdir(path)
- Returns a tuple of
(dirs, files)
representing the directories and files within the directorypath
, respectively. Note that the elements within the listsdirs
andfiles
are string objects, and not os.path objects. The entries'.'
and'..'
are never included indirs
If
path
is not a directory, this function throws an OSError - os.glob(path)
- Returns a list of string paths matching
path
, which is a glob. Essentially, wildcard expandspath
to match anything fitting that pattern. - os.stat(path)
- Queries information about the file or directory
path
on the filesystem. This is a type that can be used as a function to perform such a queryIt has the following attributes:
- .dev
- Device ID, which is typically encoded as major/minor versions. This is OS-specific typically
- .inode
- The inode of the file or directory on disk as an integer
- .gid
- The group ID of the owner
- .uid
- The user ID of the owner
- .size
- The size, in bytes, of the file
- .mode
- The mode of the file, represented as a bitmask integer
- .mtime
- The time of last modification, in seconds-since-epoch, as a floating point number (see the time module)
- .atime
- The time of last access, in seconds-since-epoch, as a floating point number (see the time module)
- .ctime
- The time of last status change, in seconds-since-epoch, as a floating point number (see the time module)
- os.lstat(path)
- Queries information about the file or link or directory
path
on the filesystem. Equivalent to os.stat, except for the fact that this function does not follow symbolic links (and thus, if called with a symbolic link, queries information about the link itself, rather than the path that it points to). Returns an os.stat object - os.fstat(fd)
- Queries information about an open file descriptor
fd
(which is expected to be an integer, or convertible to one). Returns an os.stat objectExamples:
>>> os.fstat(os.stdin) <os.stat ...> >>> os.fstat(os.stdin.fileno) # os.stdin.fileno == os.stdin as int <os.stat ...>
- os.pipe()
- Creates a new pipe, and returns a tuple of
(readio, writeio)
, which are the readable and writeable ends of the pipe, respectively. Both objects are of type io.RawIO - os.dup(fd, to=-1)
- Duplicates an open file descriptor
fd
(which should be an integer, or convertible to one)If
to < 0
, then this function creates a new file descriptor and returns the corresponding io.RawIO object. Otherwise, it replacesto
with a copy of the source offd
- os.exec(cmd)
- Executes
cmd
(which should either be a string, or a list of strings) as if it was typed into the system shell, and return the exit code as an integerSee os.proc type for more fine grain control of process spawning
- os.fork()
- Forks the current process, resulting in two processes running the code afterwards. This function returns
0
in the child process, and the PID (process ID) in the parent process (which should be an integer greater than 0)This function is available on the following platforms:
- linux
- macos
- unix
- os.mutex()
- This function creates a lock which can be used to restrict access to certain data or code. This is a type which can be used as a function to create an instance
Each mutex starts out unlocked, and then can be locked (or attempted to lock via various functions)
- os.mutex.lock(self)
- Locks the mutex, waiting until any other threads which hold the lock to unlock it before it is acquired
If the same thread which is attempting to lock the mutex has already locked it, a "deadlock" may occur and your program may halt
- os.mutex.trylock(self)
- Locks the mutex, if it can be locked instantly, otherwise do nothing. Returns whether it succesfully locked. The mutex should only be unlocked if this function returned true
Example:
>>> mut = os.mutex() >>> if mut.trylock() { ... # Acquired lock, do stuff... ... mut.unlock() # Must unlock! ... } else { ... # Failed to acquire lock, do other stuff... ... # Do not unlock! ... }
- os.mutex.unlock(self)
- Unlocks the mutex, waiting until any other threads which hold the lock to unlock it before it is acquired
- os.thread(of, args=(), name=none)
- Creates a new thread which runs the the function
of
with argumentsargs
(default: no arguments), with the namename
(default: auto-generate a name). This is a type which can be called like a function to create an instanceThe thread starts out inactive, you must call
.start()
to actually begin executing- os.thread.start(self)
- Starts executing the thread
- os.thread.join(self)
- Waits for a thread to finish executing, blocking until it does
- os.thread.isalive(self)
- Polls the thread, and returns a boolean indicating whether the thread is currently alive and executing
- os.proc(argv)
- Creates a new process with the given arguments
argv
, which can be either a string, or a list of strings representing the arguments. This is a type which can be called to create an instance.- .pid
- The process ID, as an integer
- .stdin
- The standard input of the process, as either an io.RawIO or io.FileIO (depending on launch configuration). This is writeable, and can be used to send input to the process
- .stdout
- The standard output of the process, as either an io.RawIO or io.FileIO (depending on launch configuration). This is readable, and can be used to read output from the process
- .stderr
- The standard error output of the process, as either an io.RawIO or io.FileIO (depending on launch configuration). This is readable, and can be used to read error output from the process
- os.proc.join(self)
- Waits for the process to finish executing, and returns the exit code of the process as an integer
- os.proc.isalive(self)
- Polls the process and returns a boolean describing whether the process is still alive and running
- os.proc.signal(self, code)
- Sends an integer signal to the process
- os.proc.kill(self)
- Attempts to kill the process by sending the 'KILL' signal (typically
9
) to the process
- os.path(path='.', root=none)
- Creates a path object, which acts similarly to a string (i.e. can be passed to functions such as os.chdir, os.stat, and so on) but also can be manipulated at higher levels than a string, which makes it easier to traverse the filesystem, and makes for more readable code than interpreting os.stat results
A distinction should be made between absolute paths (i.e. those which unambiguously refer to a single file on disk) and relative paths (i.e. those which require a working directory to resolve completely). To convert a string or os.path object to its absolute path, you can use the os.real function. Or, the builtin abs function works on os.path objects.
- .root
- Either
none
(for relative paths), or a string representing the start of an absolute pathSince some platforms (most Unix-like ones) use
/
as the root for the filesystem, and other platforms (such as Windows) use drive letters to denote absolute paths (C:\
,D:\
, etc), the.root
may be any of those valid roots, depending on which platform you are on. On all platforms, relative paths haveroot==none
Examples:
>>> os.path('/path/to/file.txt').root '/' >>> os.path('relative/path/to/file.txt').root none
- .parts
- A tuple containing string parts of the path, which is implicitly seperated by directory seperators.
Examples:
>>> os.path('/path/to/file.txt').parts ('path', 'to', 'file.txt') >>> os.path('relative/path/to/file.txt').parts ('relative', 'path', 'to', 'file.txt')
3.2. io: Input/Output
- io.Seek
- io.fdopen(fd, mode='r', src=none)
- io.BaseIO()
- io.RawIO(src, mode='r')
- io.FileIO(src, mode='r')
- io.StringIO(obj='')
- io.BytesIO(obj='')
The input/output module (io
) provides functionality related to text and binary streams, and allows generic processing on streams from many sources.
Specifically, it provides file access (through io.FileIO), as well as in-memory stream-like objects for text (io.StringIO), as well as bytes (io.BytesIO). These types have similar interfaces such that they can be passed to functions and operated on generically. For example, you could write a text processor that iterates over lines in a file and performs a search (like grep
), and then a caller could actually give a io.StringIO and the search method would work exactly the same. Similarly, it is often useful to build up a file by using io.BaseIO.write(), so that when outputting to a file, large temporary strings are not being built. However, sometimes you want to be able to build without creating a temporary file -- you can substitute an io.StringIO and then convert that to a string afterwards.
- io.Seek
This type is an enum of
whence
values for various seek calls (see io.BaseIO.seek). Their values are as follows:- io.Seek.SET
- Represents a reference point from the start of the stream
- io.Seek.CUR
- Represents a reference point from the current position in the stream
- io.Seek.END
- Represents a reference point from the end of the stream
- io.fdopen(fd, mode='r', src=none)
- Opens an integral file descriptor (
fd
) as a buffered IO (a io.FileIO object), with a readable name (src
) (optional) - io.BaseIO()
- This type is an abstract type that defines the io pattern. The methods listed here can be used on all io-like objects (for example, io.FileIO, io.StringIO, and so on) and they should all behave according to this pattern and functionality. Therefore, the methods and attributes are only documented here.
You can iterate over io-pattern objects, which iterates through the lines (seperated by
'\n'
characters). For example, to iterate through lines of standard input (os.stdin), you can use:for line in os.stdin { # do stuff with 'line' }
- io.BaseIO.read(self, sz=none)
- Reads a message, of up to
sz
length (returns truncated result ifsz
was too large). If size is ommitted, the rest of the stream is read and returned as a single chunkFor text-based IOs,
sz
gives the number of characters to read. For binary-based IOs,sz
gives the number of bytes to read. - io.BaseIO.write(self, msg)
- Writes a message to an io
If
msg
does not match the expected type (str for text-based IOs, and bytes for binary-based IOs), and no conversion is found, then an IOError is thrown - io.BaseIO.seek(self, pos, whence=io.Seek.SET)
- Seek to a given position (
pos
) from a given reference point (see io.Seek) - io.BaseIO.tell(self)
- Returns an integer describing the current position within the stream, from the start (i.e.
0
is the start of the stream) - io.BaseIO.trunc(self, sz=0)
- Attempts to truncate a stream to a given size (default: truncate complete to empty)
- io.BaseIO.eof(self)
- Returns a boolean indicating whether the end-of-file (EOF) has been reached
- io.BaseIO.close(self)
- Closes the IO and disables further reading/writing
- io.BaseIO.printf(self, fmt, *args)
- Print formatted text to
self
. Does not include a line break or spaces between argumentsSee printf for documentation on
fmt
and arguments
- io.RawIO(src, mode='r')
- Represents an unbuffered io, which is from either a file on disk, or a simulated file (for example, such as the result from os.pipe()). The constructor creates one from a file on disk, and behaves similar to the open function
This is a subtype of io.BaseIO, and implements the pattern fully. Additionally, this type has the following attributes:
- io.FileIO(src, mode='r')
- Represents buffered io, which is from either a file on disk, or a simulated file (for example, such as the result from os.pipe()). The constructor creates one from a file on disk, and is equivalent to the open function.
This is a subtype of io.BaseIO, and implements the pattern fully. Additionally, this type has the following attributes:
- .fileno
- This attribute retrieves the file descriptor associated with the stream
For example, on most systems:
>>> os.stdin.fileno 0 >>> os.stdout.fileno 1 >>> os.stderr.fileno 2
- io.StringIO(obj='')
Represents an io for textual information, being built in memory (i.e. not as a file on disk). It can be used in places where io.FileIO is typically used.
This is a subtype of io.BaseIO, and implements the pattern fully. Additionally, this type has the following methods:
- io.StringIO.get(self)
- Get the current contents as a str
- io.BytesIO(obj='')
Represents an io for byte-based information, being built in memory (i.e. not as a file on disk). It can be used in places where io.FileIO is typically used.
This is a subtype of io.BaseIO, and implements the pattern fully. Additionally, this type has the following methods:
- io.BytesIO.get(self)
- Get the current contents as a bytes
3.3. net: Networking
- net.http: HTTP Networking: HTTP-specific networking submodule
- net.FK
- net.SK
- net.PK
- net.SocketIO(fk=net.FK.INET4, sk=net.SK.TCP, pk=net.PK.AUTO)
The network module (net
) provides functionality related to the world wide web, and other networks (i.e. LANs).
- net.FK
- This type represents the type of family of address/connection/network
- net.FK.INET4
- IPv4 style addresses.
Addresses for this type of socket are expected to be a [
tuple
](/builtins#tuple) containing(host, port)
, wherehost
is a string hostname/IP, andport
is an integer. - net.FK.INET6
- IPv6 style addresses.
Addresses for this type of socket are expected to be a [
tuple
](/builtins#tuple) containing(host, port, flow, scope)
, wherehost
is a string hostname/IP, andport
is an integerTODO: This is not yet implemented
- net.FK.BT
- Bluetooth style addresses.
TODO: This is not yet implemented
- net.SK
- This type represents the type of socket
- net.SK.RAW
- Raw socket kind, which sends raw packets
- net.SK.TCP
- TCP/IP socket kind, which goes through the TCP/IP network protocol
- net.SK.UDP
- UDP socket kind, which goes through the UDP network protocol
- net.SK.PACKET
- Packet socket kind
This kind of socket is deprecated
- net.SK.PACKET_SEQ
- Packet (sequential) socket kind
- net.PK
- This type represents the type of protocol used in transmission
- net.PK.AUTO
- Automatic protocol (which is a safe default)
- net.PK.BT_L2CAP
- Bluetooth protocol
- net.PK.BT_RFCOMM
- Bluetooth protocol
- net.SocketIO(fk=net.FK.INET4, sk=net.SK.TCP, pk=net.PK.AUTO)
- This type represents a network socket, which is an endpoint for sending/receiving data across a network. There are different types of sockets, but the most commons are the default arguments. You can manually specify the family, socket, and protocol used by supplying them, they should be members of the enums net.FK, net.SK, and net.PK respectively. The dfeault is IPv4, TCP/IP, and automatic protocol.
This is a subtype of io.BaseIO, and implements the pattern fully. Additionally, this type has the following methods:
- net.SocketIO.bind(self, addr)
- Binds the socket to the given address
The type expected for
addr
depends on the family kind of socket thatself
is - net.SocketIO.connect(self, addr)
- Connects the socket to the given address
The type expected for
addr
depends on the family kind of socket thatself
is - net.SocketIO.listen(self, num=16)
- Begins listening for connections, and only allows
num
to be active at once before refusing connections - net.SocketIO.accept(self)
- Accepts a new connection, returning a tuple of
(sock, name)
, wheresock
is a net.SocketIO object that can be read from and written to, andname
is a string representing the client's name
3.3.1. net.http: HTTP Networking
- net.http.Request(method, uri, httpv, headers, body)
- net.http.Response(httpv, code, headers, body)
- net.http.Server(addr)
- net.http.uriencode(text)
- net.http.uridecode(text)
The HTTP networking submodule (net.http
) is a submodule of the net module. Specifically, it provides HTTP utilities built on top of the rest of the networking stack.
- net.http.Request(method, uri, httpv, headers, body)
This type represents an HTTP request
It has the following attibutes:
- .method
- A string representing the HTTP method
See here for a list of valid methods
- .uri
- A string representing the requested path. Includes a leading
/
.For example, a
GET
request tomysite.com/path/to/dir.txt
would have'/path/to/dir.txt'
as the.uri
componentYou can use the functions net.http.uriencode and net.http.uridecode to encode/decode components of a URI (for example, replaces reserved characters with
%
escapes) - .httpv
- A string representing the HTTP protocol version. This is almost always
'HTTP/1.1'
- .headers
- A dict representing key-val entries for the headers
- .body
- A bytes representing the request body (may be empty)
- net.http.Response(httpv, code, headers, body)
- This type represents an HTTP response
It has the following attibutes:
- .httpv
- A string representing the HTTP protocol version. This is almost always
'HTTP/1.1'
- .code
- An integer representing the status code. Common values are
200
(OK),404
(NOTFOUND) - .headers
- A dict representing the key-val pairs of headers
- .body
- A bytes object representing the body of the response
- net.http.Server(addr)
- This type represents an HTTP server, which binds itself to
addr
.Commonly, to host a local server, you will want to give
addr
as("localhost", 8080)
(or whatever port you want to host on).Once you've created a server, you should call
serverobj.serve()
(net.http.Server.serve()) to serve forever in the current thread. When a new connection is requested, it spawns a new thread and serves each request in a new thread.Example:
>>> s = net.http.Server(("localhost", 8080)) >>> s.serve() # Hangs in the current thread, but spawns new ones to handle requests
- net.http.Server.serve(self)
- Serve forever on the current thread, spawning new threads that call net.http.Server.handle() for each request
- net.http.Server.handle(self, addr, sock, req)
- The request callback, which is called everytime a request is made to the server
It is given the arguments:
addr
: A string representing the client's addresssock
: The socket/socket-like IO (commonly an net.SocketIO) that can be used to communicate with the clientreq
: A request object (specifically, of the type net.http.Request), holding the information about the request
This method should typically not write to
sock
. Instead, it should return either a string, bytes, or a net.http.Response object, which will then be automatically written to the socket afterwards.Here's an example that just returns the path requested:
func handle(self, addr, sock, req) { ret "You asked for: %s" % (req.uri,) }
- net.http.uriencode(text)
This function takes a string,
text
, and encodes reserved characters with appropriate escape codes (see here)ASCII characters which are not escaped are added to the output unchanged; all non-ASCII characters are converted to their UTF-8 byte sequence, and
%
escaped each byte sequenceFor the inverse of this function, see net.http.uridecode
Examples:
>>> net.http.uriencode('hey there everyone') 'hey%20there%20everyone' >>> net.http.uriencode('I love to eat \N[GREEK SMALL LETTER PI]') 'I%20love%20to%20eat%20%CF%80'
- net.http.uridecode(text)
This function takes a string,
text
, and decodes reserved characters from appropriate escape codes (see here)Escape sequences outside of the normal ASCII range are taken to be UTF8, and decoded as such
For the inverse of this function, see net.http.uriencode
Examples:
>>> net.http.uridecode('hey%20there%20everyone') 'hey there everyone' >>> net.http.uridecode('I%20love%20to%20eat%20%CF%80') 'I love to eat π'
3.4. time: Time
- time.ISO8601
- time.time()
- time.clock()
- time.sleep(dur)
- time.now()
- time.localnow()
- time.format(val=none, fmt=time.ISO8601)
- time.DateTime(obj=none, tz=none)
This module, time
, allows code to programmatically determine the current time, convert date-times to human readable formats, create timestamps, and reason about multiple times.
- time.ISO8601
This is a string which is the format that can be passed to time.format and time.parse to format and parse ISO8601 format. This format is the preferred format for exchanges of dates and times.
- time.time()
- Returns the number of seconds since the Epoch as a floating point number.
The Epoch may depend on your platform, but is most commonly 1970-01-01. You can check when the epoch is by passing
0
to time.format:>>> time.format(0.0) '1970-01-01T00:00:00+0000'
- time.clock()
Returns the number of seconds since the process started as a floating point number.
- time.sleep(dur)
- Causes the current thread to sleep for
dur
seconds, which should be a floating point numberThe exact accuracy of this function cannot be guaranteed, it depends on the platform function. For example, if
nanosleep()
orusleep()
are available in the C library, this function will be accurate. At worst, this function will only sleep to the nearest second - time.now()
- Returns a time.DateTime referring to the current time, in UTC
See time.localnow for local-equivalent function
- time.localnow()
- Returns a time.DateTime referring to the current time, in the local timezone
See time.now for UTC-equivalent function
- time.format(val=none, fmt=time.ISO8601)
- Returns a string which is the time value
val
(default: time.now(), which may be a floating point number, or a time.DateTime object) formatted according tofmt
.The format string is similar to printf syntax, but with different seperators:
- %%
- Literal
%
- %Y
- The year, in full
- %y
- The year, modulo 100 (i.e.
1970
would result in70
) - %m
- The month of the year (starting at 1), zero-padded to 2 digits (i.e. February would be
02
) - %B
- The month of the year, in the current locale, full
- %b
- The month of the year, in the current locale, abbreviated
- %U
- The week of the year as a decimal number (Sunday is
0
). Days before the first Sunday are week 0 - %W
- The week of the year as a decimal number (Monday is
0
). Days before the first Monday are week 0 - %j
- The day of the year as a decimal number (starting with
001
), zero-padded to 3 digits - %d
- The day of the month as a decimal number (starting with
01
), zero-padded to 2 digits - %A
- The day of the week, in the current locale, full
- %a
- The day of the week, in the current locale, abbreviated
- %w
- The day of the week as an integer (Monday is
0
) - %H
- Hour (in 24-hour clock), zero-padded to 2 digits (
00
, ...,23
) - %I
- Hour (in 12-hour clock), zero-padded to 2 digits (
00
, ...,12
) - %M
- Minute, zero-padded to 2 digits (
00
, ...,59
) - %S
- Second, zero-padded to 2 digits (
00
, ...,59
) - %f
- Microsecond, zero-padded to 6 digits (
000000
, ...,999999
) - %z
- Timezone UTC offset in
(+|-)HHMM[SS.[ffffff]]
- %Z
- Timezone name (or empty if there was none)
- %p
- Current locale's equivalent of
AM
andPM
- %c
- Current locale's default date/time representation
- %x
- Current locale's default date representation
- %X
- Current locale's default time representation
- time.DateTime(obj=none, tz=none)
-
This type represents a broken-down time structure comprised of the attributes humans commonly associate with a time. For example, the year, month, day, and so forth.
DateTimes can be created with the empty constructor,
time.DateTime()
, which is equivalent to the time.now function. Or, you can pass the first argument as a number of seconds since the Epoch (i.e. what is returned by time.time). For example,time.DateTime(0.0)
will give you a DateTime representing the system Epoch.The constructor also accepts another argument,
tz
, which is the timezone. If not given, or none, the resulting datetime is in UTC (which is to say, a reasonable default). To get a datetime in local time, you can pass'local'
as the second argument. You can also give it a specific name of a timezone, and it will attempt to use that timezone.This type is not a completely consistent datatype (as it must deal with things like daylight savings, leap seconds, leap year, and so forth), so it is recommended to use a float to capture absolute times, and deal with timestamps.
Here are some examples of creating datetimes in various ways (the output may differ based on your location, obviously!):
>>> time.DateTime() # Current datetime, in UTC <time.DateTime '2021-01-13T01:10:26+0000'> >>> time.DateTime(0) # Epoch, in UTC <time.DateTime '1970-01-01T00:00:00+0000'> >>> time.DateTime(0, "local") # Epoch, in current timezone <time.DateTime '1969-12-31T19:00:00-0500'>
- .tse
- The time since Epoch, in number of seconds as a floating point number
- .tz
- The timezone, which may be a string (the name), or
none
if there was no timezone - .year
- The year as an integer
- .month
- The month as an integer
- .day
- The day as an integer
- .hour
- The hour as an integer
- .min
- The minute as an integer
- .sec
- The second as an integer
- .nano
- The nanosecond as an integer
3.5. util: Utilities
This module, util
, implements commonly used datastructures and algorithms that aren't in the builtins. While they are commonly used, they are not used frequently enough to use the global namespace and thus restrict developers from using those names in their own code. So, you can think of the util
module as "builtins 2: electric boogaloo".
- util.Queue(objs=none)
This type represents a queue which can handle arbitrary objects. It can be created via the constructor, which accepts
objs
(default: none), which is expected to be an iterable containing the elements to initialize the queue from.The main purpose of a queue over the builtin list type is that certain operations are much more efficient for a queue. For example, popping from the left and right is $O(1)$, whereas with a list they are $O(N)$ and $O(1)$ respectively (where $N$ is the length of the collection). This has large consequences if you are doing the operation over and over for example -- using a queue will reduce runtime drastically.
A queue is iterable just like a list, and it iterates in the same order as a list.
- util.Queue.pop(self)
- Pops from the front of the queue (i.e. the
0
th element)This can be confusing, since list.pop pops from the back of the list (i.e. the last element). Keep in mind the differences
- util.Queue.push(self, *args)
- Pushes all of
args
to the back of the queue
- util.BST(objs=none)
- This type represents a key-value mapping following the dict pattern, implemented using a Binary Search Tree (BST). Unlike dict, however, keys must be comparable (not neccessarily hashable), and are stored in sorted order (as opposed to insertion order). This is important for algorithms which maintain a sorted list or mapping.
You can construct a BST with the constructor, which accepts a dict -like mapping, which inserts every key/value pair into a sorted mapping.
Values for keys can be retreived via indexing:
x[k]
gets the value associated with keyk
in the BSTx
, or throws a KeyError if no such key exists. Likewise,x[k] = v
adds (or replaces) the value associated with keyk
in the BSTx
with valuev
.You can iterate over the keys in a binary search tree as well:
>>> x = util.BST({2: 'a', 1: 'b'}) util.BST({1: 'b', 2: 'a'}) >>> for k in x, print (k, x[k]) 1 b 2 a
Examples:
>>> x = util.BST({2: 'a', 1: 'b'}) util.BST({1: 'b', 2: 'a'}) >>> 1 in x true >>> 3 in x false >>> x[0] = 'hey' >>> x util.BST({0: 'hey', 1: 'b', 2: 'a'})
- util.Bitset(objs=none)
-
This type represents a bitset (also called "bit map", "bit array", etc). The general idea is that it can store whether positive integers are in a set or not based on a single integer mask. This type is supposed to behave exactly like the builtin set type, except it only supports positive integers, and uses $O(max(X))$ memory (where $X$ are the elements in the set). Sets use $O(N)$ memory, where $N$ is the number of elements in the set.
Bitsets can be created with an iterable
objs
(default: none), in which case all elements are converted to an integer and added to the set. Additionally, a bitset can be created with a single integer, which represents the bit mask of the entire set (see below).If we look at a number decomposed into bits, we can say that if the
i
th bit is set to1
, theni
is in the set, and otherwise,i
is not in the set. For example, the integer0b11011
corresponds to the integers0
,1
,3
, and4
.You can convert a bitset into the corresponding integer like so:
>>> util.Bitset([0, 1, 3, 4]) as int 27 >> bin(27) # Check binary notation 0b11011
The main advantage of using a bitset over a normal set with integers is speed of common operations. For example, intersection, union, difference, and so forth can be computed extremely efficiently, perhaps 10x faster. So, this type is available for those use cases.
- util.Graph(nodes=none, edges=none)
-
This type is currently being implemented, so this documentation is incomplete.
3.6. gram: Grammar
The grammar module (gram
) provides commonly needed types and algorithms for dealing with computer grammars (for example, descriptions of programming language syntax)
- gram.Lexer(patterns, src=os.stdin)
- This type represents a lexer/tokenizer which token rules are defined as either str literals or regex patterns.
The constructor takes an iterable of rules -- each rule is expected to be a tuple of length 2, containing
(pattern, action)
.pattern
may be a str or regex; if it is a string, then the token matches that string literal exactly, and otherwise the token matches the regex pattern.action
may be a function (in which case the result of a token being found is the result ofaction(tokenstring)
). Otherwise, it is may be an integral object, which is the token kind. Commonly, these may be members of an enumeration meant to represent every token kind in a given context. Otherwise,action
is expected to benone
, in which case the potential token is discarded and the characters that made up that token are ignored. The rule that is chosen is that which generates the longest match from the current position in a file. If two rules match the same length, then the one given first in therules
variable is used first.The second argument,
src
, is expected to be an object similar to io.BaseIO (default is os.stdin). This is the source from which characters are taken.This is hard to grasp abstractly -- here is an example recognizing numbers and words, and ignoring everything else:
>>> L = gram.Lexer([ ... (`[:alpha:]+`, 0), ... (`[:digit:]+`, 1), ... (`.`, none). ... (`\n`, none) ... ], io.StringIO("hey 123 456 test")) gram.Lexer([(regex('[[:alpha:]_][[:alpha:]_[:digit:]]*'), 0), (regex('\\d+'), 1), (regex('.'), none), (regex('\\n'), none)], <'io.StringIO' @ 0x55A6E90CE990>) >>> next(L) gram.Token(0, 'hey') >>> next(L) gram.Token(1, '123') >>> next(L) gram.Token(1, '456') >>> next(L) gram.Token(0, 'test') >>> next(L) OutOfIterException: Call Stack: #0: In '<expr>' (line 1, col 1): next(L) ^~~~~~~ #1: In next(obj) [cfunc] #2: In gram.Lexer.__next(self) [cfunc] In <thread 'main'>
You can also iterate over a lexer to produce the token stream:
>>> L = gram.Lexer([ ... (`[:alpha:]+`, 0), ... (`[:digit:]+`, 1), ... (`.`, none). ... (`\n`, none) ... ], io.StringIO("hey 123 456 test")) gram.Lexer([(regex('[[:alpha:]_][[:alpha:]_[:digit:]]*'), 0), (regex('\\d+'), 1), (regex('.'), none), (regex('\\n'), none)], <'io.StringIO' @ 0x55A6E90CE990>) >>> for tok in L, print(repr(tok)) gram.Token(0, 'hey') gram.Token(1, '123') gram.Token(1, '456') gram.Token(0, 'test')
3.7. m: Math
- m.pi
- m.tau
- m.e
- m.mascheroni
- m.isclose(x, y, abs_err=1e-6, rel_err=1e-6)
- m.floor(x)
- m.ceil(x)
- m.round(x)
- m.sgn(x)
- m.sqrt(x)
- m.exp(x)
- m.log(x, b=m.e)
- m.rad(x)
- m.deg(x)
- m.hypot(x, y)
- m.sin(x)
- m.cos(x)
- m.tan(x)
- m.sinh(x)
- m.cosh(x)
- m.tanh(x)
- m.asin(x)
- m.acos(x)
- m.atan(x)
- m.asinh(x)
- m.acosh(x)
- m.atanh(x)
- m.erf(x)
- m.erfc(x)
- m.gamma(x)
- m.zeta(x)
- m.modinv(x, n)
- m.gcd(x, y)
- m.egcd(x, y)
This module, m
, provides functionality to aid in mathematical problems/needs .This module contains common mathematical constants (such as $\pi$, $\tau$, $e$, and so forth), as well as functions that efficiently and accurately compute commonly used functions (such as $\sin$, $\cos$, $\Gamma$, and so forth). This module also includes some integer and number-theoretic functions, such as computing the greatest common denominator (GCD), binomial coefficients, and primality testing.
This module is meant to work with the types that follow the number pattern, such as int, float, and complex. Most functions are defined for real and complex evaluation. If a real number is given, then (generally) a real number is returned. If a complex number is given, then (generally) a complex number is returned. If a real number is given (for example, to m.sqrt) and the result would be a complex number (i.e. m.sqrt(-1)
), then an error is thrown (this makes it easy to find bugs, and in general real numbers are what most people care about – and they would like an error on code such as m.sqrt(-1)
). To get around this, you can write: m.sqrt(complex(-1))
, and the result will always be a complex, and an error won’t be thrown for negative numbers.
Constants are given in maximum precision possible within a float, but for all of these constants, their value is not exact. This causes some issues or unexpected results. For example, mathematically, $\sin(\pi) = 0$, but m.sin(m.pi) == 1.22464679914735e-16
. This is expected when using finite precision. Just make sure to keep this in mind.
Here are some recommended ways to handle it:
>>> if m.sin(m.pi) == 0 {} # Bad, may cause unexpected results
>>> if abs(m.sin(m.pi) - 0) < 1e-6 {} # Better, uses a decent tolerance (1e-6 is pretty good)
>>> if m.isclose(m.sin(m.pi), 0) {} # Best, use the m.isclose() function
- m.pi
-
The value of $\pi$, as a float
>>> m.pi 3.141592653589793
- m.tau
-
The value of $\tau$, as a float
>>> m.tau 6.283185307179586
- m.e
-
>>> m.e 2.718281828459045
- m.mascheroni
-
The value of $\gamma$, as a float
>>> m.mascheroni 0.577215664901533
- m.isclose(x, y, abs_err=1e-6, rel_err=1e-6)
-
Computes whether
x
andy
are "close", i.e. withinabs_err
absolute error or having a relative error ofrel_err
.Is equivalent to:
func isclose(x, y, abs_err=1e-6, rel_err=1e-6) { ad = abs(x - y) ret ad <= abs_err || ad <= abs_err * max(abs(x), abs(y)) }
- m.floor(x)
- m.ceil(x)
- Computes the ceiling of
x
, as an int - m.round(x)
- Computes the nearest int to
x
, rounding towards+inf
if exactly between integers - m.sgn(x)
- Computes the sign of
x
, returning one of+1
,0
, or-1
- m.sqrt(x)
- Computes the square root of
x
.If
x
is a real type (i.e.int
orfloat
) then negative numbers will throw a MathError. You can instead write:m.sqrt(complex(x))
, which will give complex results for negative numbers - m.exp(x)
- Computes the expontial function (base-$e$) of
x
- m.log(x, b=m.e)
- Computes the logarithm (base-$b$) of
x
. Default is the natural logarithm - m.rad(x)
- Converts
x
(which is in degrees) to radians - m.deg(x)
- Converts
x
(which is in radians) to degrees - m.hypot(x, y)
- Computes the side of a right triangle with sides
x
andy
- m.sin(x)
- Computes the sine of
x
(which is in radians) - m.cos(x)
- Computes the cosine of
x
(which is in radians) - m.tan(x)
- Computes the tangent of
x
(which is in radians) - m.sinh(x)
- Computes the hyperbolic sine of
x
(which is in radians) - m.cosh(x)
- Computes the hyperbolic cosine of
x
(which is in radians) - m.tanh(x)
- Computes the hyperbolic tangent of
x
(which is in radians) - m.asin(x)
- Computes the inverse sine of
x
(which is in radians) - m.acos(x)
- Computes the inverse cosine of
x
(which is in radians) - m.atan(x)
- Computes the inverse tangent of
x
(which is in radians) - m.asinh(x)
- Computes the inverse hyperbolic sine of
x
(which is in radians) - m.acosh(x)
- Computes the inverse hyperbolic cosine of
x
(which is in radians) - m.atanh(x)
- Computes the inverse hyperbolic tangent of
x
(which is in radians) - m.erf(x)
- Computes the error function of
x
Defined as $\frac{2}{\sqrt \pi} \int_{0}^{x} e^{-t^2} dt$
- m.erfc(x)
- Computes the complimentary error function of
x
, defined as1 - m.erf(x)
- m.gamma(x)
- Computes the Gamma function of
x
, $\Gamma(x)$ - m.zeta(x)
- Computes the Riemann Zeta function of
x
, $\zeta(x)$ - m.modinv(x, n)
- Computes the modular inverse of
x
within the ring of integers modulon
(i.e. $Z_n$)A MathError is thrown if no such inverse exists
- m.gcd(x, y)
- Computes the Greatest Common Divisor (GCD) of
x
andy
- m.egcd(x, y)
- Computes the Extended Greatest Common Divisor (EGCD) of
x
andy
, returning a tuple of(g, s, t)
such thatx*s + y*t == g == m.gcd(x, y)
If
abs(x) == abs(y)
, then(g, 0, m.sgn(y))
is returned
3.8. nx: NumeriX
- nx.la: Linear Algebra: Linear Algebra Submodule
- nx.fft: FFT: FFT Submodules
- nx.array(objs=[], dtype=nx.double)
- nx.dtype
- nx.zeros(shape=none, dtype=nx.double)
- nx.ones(shape=none, dtype=nx.double)
- nx.pad(x, shape)
- nx.onehot(x, newdim=none, r=none)
- nx.abs(x, r=none)
- nx.conj(x, r=none)
- nx.neg(x, r=none)
- nx.add(x, y, r=none)
- nx.sub(x, y, r=none)
- nx.mul(x, y, r=none)
- nx.div(x, y, r=none)
- nx.floordiv(x, y, r=none)
- nx.mod(x, y, r=none)
- nx.pow(x, y, r=none)
- nx.exp(x, r=none)
- nx.log(x, r=none)
- nx.sqrt(x, r=none)
- nx.min(x, axes=none, r=none)
- nx.max(x, axes=none, r=none)
- nx.sum(x, axes=none, r=none)
- nx.prod(x, axes=none, r=none)
- nx.cumsum(x, axis=-1, r=none)
- nx.cumprod(x, axis=-1, r=none)
- nx.sort(x, axis=-1, r=none)
- nx.cast(x, dtype, r=none)
- nx.fpcast(x, dtype, r=none)
The NumeriX module (nx
) provides array operations, linear algebra algorithms, transforms, and other numerical algorithms. It is similar to the math module (m
), but is intended to work for specific datatypes and compute the result in parallel for entire arrays. This is sometimes called array programming.
Using nx
can result in more readable and more efficient code. And, since it is part of the standard library of kscript, it can be used anywhere without installing additional packages.
However, this package is complicated and large, so before looking at documentation, you should read this section so you can be sure you understand the concepts presented. Here are the main concepts present in this module:
- Arrays, described by type nx.array. This is the basis of all data processed by this module
- nx.array(objs=[], dtype=nx.double)
- This type represents a multi-dimensional array (sometimes called as a tensor), which has elements and an associated datatype. Operations on this type typically act as vectorized operations -- which means applying the operation to every element, in one batch job. This means that the code can be more efficient internally, whereas if you write a for-loop to manually perform the operation, it may be 100 or 1000 times slower!
Arrays (approximately) follow the number pattern for 0-rank arrays. But, this is not to be relied upon. In general, when programming for array-based input, you should have them follow the nx.array pattern, which will treat number objects as a rank-0 array. In other words, you can think of the nx.array pattern as a generalization of the number pattern.
- .rank
- This attribute gives the rank of the array as an integer
The rank of an array is the number of dimensions (i.e. axes) it has. For example, a matrix would be considered rank-2 (2 dimensional), so it would have a rank of 2.
Scalar values are 0-rank
In documentation, sometimes arrays are described as rank-N, or ND (N-dimensional). These are equivalent
- .shape
- This attribute gives the shape of the array as a tuple of integers
The shape of an array is the length in each dimension, in number of elements. For example, a matrix would be considered rank-2 (2 dimensional), and so the shape would be of the form
(M, N)
, whereM
andN
are integers.Scalar values are 0-rank, and have a shape of
()
, the empty tuple. - .strides
- This attribute gives the stride of the array as a tuple of integers. This is rarely useful for most developers.
The stride of an array is the distance between each element of that dimension, in bytes, as an integer.
- .T
- This attribute gives a view of the transpose of an array. There are a few cases:
- If the array is rank-0, then it is given unchanged
- If the array is rank-1, then it is treated like a row-vector, and a column vector of shape
(N, 1)
is given - Otherwise, the last two axes are swapped (the input is treated like a stack of matrices)
- nx.dtype
- This type represents a datatype, which arrays can hold elements of. Typically, these correspond to types in the C library, or hardware types for a specific platform.
Datatypes can represent sized-integer values, floating point values, complex floating point values, or structures of other datatypes. Most mathematical operations are only supported on the builtin numeric datayptes (which includes integer, float, and complex datatypes), which support by platform may vary.
- nx.zeros(shape=none, dtype=nx.double)
- Create an array of zeros with a shape of
shape
(default: scalar), and datatype (default: double) - nx.ones(shape=none, dtype=nx.double)
- Create an array of ones with a shape of
shape
(default: scalar), and datatype (default: double) - nx.pad(x, shape)
- Pads
x
to a given shape (shape
, which should match the rank ofx
). Expandsx
to dimensions which are larger (and adds zeros), and shrinksx
to dimensions which are smaller (and omits values) - nx.onehot(x, newdim=none, r=none)
- Computes a one-hot encoding, where
x
are the indices,newdim
is the size of the new dimension which the indices point to (default: last dimension ofx
), storing inr
(default: return new array). Indices inx
are taken modulonewdim
- nx.abs(x, r=none)
- Computes elementwise absolute value of
x
, storing inr
(default: return new array) - nx.conj(x, r=none)
- Computes elementwise conjugation of
x
, storing inr
(default: return new array) - nx.neg(x, r=none)
- Computes elementwise negation of
x
, storing inr
(default: return new array) - nx.add(x, y, r=none)
- Computes elementwise addition of
x
andy
, storing inr
(default: return new array) - nx.sub(x, y, r=none)
- Computes elementwise subtraction of
x
andy
, storing inr
(default: return new array) - nx.mul(x, y, r=none)
- Computes elementwise multiplication of
x
andy
, storing inr
(default: return new array) - nx.div(x, y, r=none)
- Computes elementwise division of
x
andy
, storing inr
(default: return new array) - nx.floordiv(x, y, r=none)
- Computes elementwise floored division of
x
andy
, storing inr
(default: return new array) - nx.mod(x, y, r=none)
- Computes elementwise modulo of
x
andy
, storing inr
(default: return new array) - nx.pow(x, y, r=none)
- Computes elementwise power of
x
andy
, storing inr
(default: return new array) - nx.exp(x, r=none)
- Computes elementwise exponential function of
x
, storing inr
(default: return new array) - nx.log(x, r=none)
- Computes elementwise natural logarithm of
x
, storing inr
(default: return new array) - nx.sqrt(x, r=none)
- Computes elementwise square root of
x
, storing inr
(default: return new array) - nx.min(x, axes=none, r=none)
- Computes reduction on the minimum of
x
onaxes
(default: all), storing inr
(default: return new array) - nx.max(x, axes=none, r=none)
- Computes reduction on the maximum of
x
onaxes
(default: all), storing inr
(default: return new array) - nx.sum(x, axes=none, r=none)
- Computes reduction on the sum of
x
onaxes
(default: all), storing inr
(default: return new array) - nx.prod(x, axes=none, r=none)
- Computes reduction on the product of
x
onaxes
(default: all), storing inr
(default: return new array) - nx.cumsum(x, axis=-1, r=none)
- Computes cumuluative sum of
x
onaxes
(default: last), storing inr
(default: return new array) - nx.cumprod(x, axis=-1, r=none)
- Computes cumulative product of
x
onaxes
(default: last), storing inr
(default: return new array) - nx.sort(x, axis=-1, r=none)
- Sorts
x
onaxis
(default: last), storing inr
(default: return new array) - nx.cast(x, dtype, r=none)
- Casts
x
to a datatype, storing inr
(default: return new array) - nx.fpcast(x, dtype, r=none)
- Casts
x
to a datatype, storing inr
(default: return new array). This is like nx.cast, except that it automatically converts to and from fixed point and floating point. For example, if going from integer to float types, the result is scaled to the[0, 1]
range (unsigned values) or[-1, 1]
range (signed values). Likewise, going from float to integer types, the result is scaled to the full range of the integer type, from the floating point scale.
3.8.1. nx.la: Linear Algebra
- nx.la.norm(x)
- nx.la.diag(x)
- nx.la.perm(x)
- nx.la.matmul(x, y, r=none)
- nx.la.factlu(x, p=none, l=none, u=none)
This module is a submodule of the nx module. Specifically, it implements functionality related to dense linear algebra.
Most functions operate on arrays, which are expected to be of rank-2 or more (in which case it is a stack of matrices).
- nx.la.norm(x)
- Computes the Frobenius norm of
x
(which should be of rank-2 or more)Equivalent to
nx.sqrt(nx.sum(nx.abs(x) ** 2, (-2, -1)))
- nx.la.diag(x)
- Creates a diagonal matrix with
x
as the diagonal. Ifx
's rank is greater than 1, then it is assumed to be a stack of diagonals, and this function returns a stack of matricesExamples:
>>> nx.la.diag([1, 2, 3]) [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]] >>> nx.la.diag([[1, 2, 3], [4, 5, 6]]) [[[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]], [[4.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 6.0]]]
- nx.la.perm(x)
-
Creates a permutation matrix with
x
as the row interchanges. Ifx.rank > 1
, then it is assumed to be a stack of row interchanges, and this function returns a stack of matricesEquivalent to
nx.onehot(x, x.shape[-1])
Examples:
>>> nx.la.perm([0, 1, 2]) [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] >>> nx.la.perm([1, 2, 0]) [[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]]
- nx.la.matmul(x, y, r=none)
Calculates the matrix product
x @ y
. Ifr
is none, then a result is allocated, otherwise it must be the correct shape, and the result will be stored in that.Expects matrices to be of shape:
*
x
:(..., M, N)
*
y
:(..., N, K)
*
r
:(..., M, K)
(orr==none
, it will be allocated)Examples:
>>> nx.la.matmul([[1, 2], [3, 4]], [[5, 6], [7, 8]]) [[19.0, 22.0], [43.0, 50.0]]
- nx.la.factlu(x, p=none, l=none, u=none)
Factors
x
according to LU decomposition with partial pivoting, and returns a tuple of(p, l, u)
such thatx == nx.la.perm(p) @ l @ u
(within numerical accuracy), andp
gives the row-interchanges required,l
is lower triangular, andu
is upper triangular. Ifp
,l
, oru
is given, it is used as the destination, otherwise a new result is allocated.Expects matrices to be of shape:
*
x
:(..., N, N)
*
p
:(..., N)
(or ifp==none
, it will be allocated)*
l
:(..., N, N)
(or ifl==none
, it will be allocated)*
u
:(..., N, N)
(or ifu==none
, it will be allocated)Examples:
>>> (p, l, u) = nx.la.factlu([[1, 2], [3, 4]]) ([1, 0], [[1.0, 0.0], [0.333333333333333, 1.0]], [[3.0, 4.0], [0.0, 0.666666666666667]]) >>> nx.la.perm(p) @ l @ u [[1.0, 2.0], [3.0, 4.0]]
3.8.2. nx.fft: FFT
This module is a submodule of the nx module. Specifically, it provides functionality related to FFTs (Fast Fourier Transforms).
Different languages/libraries use different conventions for FFT/IFFT/other transforms. It is important the developer knows which are used by kscript, as they are explained per function.
- nx.fft.fft(x, axes=none, r=none)
- Calculates the forward FFT of
x
, uponaxes
(default: all axes), and stores inr
(or, ifr==none
, then a result is allocated)The result of this function is always complex. If an integer datatype input is given, the result will be nx.complexdouble . All numeric datatypes are supported, and are to full precision.
For a 1-D FFT, let $N$ be the size, $x$ be the input, and $X$ be the output. Then, the corresponding entries of $X$ are given by:
$$X_j = \sum_{k=0}^{N-1} x_k e^{-2 \pi i j k /N}$$
For an N-D FFT, the result is equivalent to doing a 1D FFT over each of
axes
See nx.fft.ifft (the inverse of this function)
Examples:
>>> nx.fft.fft([1, 2, 3, 4]) [10.0+0.0i, -2.0-2.0i, -2.0+0.0i, -2.0+2.0i]
- nx.fft.ifft(x, axes=none, r=none)
Calculates the inverse FFT of
x
, uponaxes
(default: all axes), and stores inr
(or, ifr==none
, then a result is allocated)The result of this function is always complex. If an integer datatype input is given, the result will be nx.complexdouble . All numeric datatypes are supported, and are to full precision.
For a 1-D IFFT, let $N$ be the size, $x$ be the input, and $X$ be the output. Then, the corresponding entries of $X$ are given by:
$$X_j = \sum_{k=0}^{N-1} \frac{x_k e^{2 \pi i j k / N}}{N}$$
Note that there is a $\frac{1}{N}$ term in the summation. Some other libraries do different kinds of scaling. The kscript fft/ifft functions will always produce the same result (up to machine precision) when given:
nx.fft.ifft(nx.fft.fft(x))
For an N-D IFFT, the result is equivalent to doing a 1D IFFT over each of
axes
See nx.fft.fft (the inverse of this function)
Examples:
>>> nx.fft.ifft([1, 2, 3, 4]) [2.5+0.0i, -0.5+0.5i, -0.5+0.0i, -0.5-0.5i]
3.9. ffi: Foreign Functions
- ffi.s8
- ffi.u8
- ffi.s16
- ffi.u16
- ffi.s32
- ffi.u32
- ffi.s64
- ffi.u64
- ffi.char
- ffi.uchar
- ffi.short
- ffi.ushort
- ffi.int
- ffi.uint
- ffi.long
- ffi.ulong
- ffi.longlong
- ffi.ulonglong
- ffi.size_t
- ffi.ssize_t
- ffi.float
- ffi.double
- ffi.longdouble
- ffi.ptr[T=none]
- ffi.func[ResultT=none, ArgsT=()]
- ffi.struct[*members]
- ffi.DLL()
- ffi.open(src)
- ffi.sizeof(obj)
- ffi.addr(obj)
- ffi.fromUTF8(addr)
- ffi.toUTF8(obj, addr=none, sz=-1)
- ffi.malloc(sz)
- ffi.realloc(ptr, sz)
- ffi.free(ptr)
The foreign function interface module, ffi
, provides functionality required to load and execute dynamically loaded code from other languages.
This module has definitions for C-style types (including pointers, structs and functions), and utilities to load libraries.
Alias types (for example, ffi.int, ffi.long, etc) are really just the specifically sized type for the current platform. For example, on most platforms, ffi.int == ffi.s32
- ffi.s8
- Signed 8 bit integer
- ffi.u8
- Unsigned 8 bit integer
- ffi.s16
- Signed 16 bit integer
- ffi.u16
- Unsigned 16 bit integer
- ffi.s32
- Signed 32 bit integer
- ffi.u32
- Unsigned 32 bit integer
- ffi.s64
- Signed 64 bit integer
- ffi.u64
- Unsigned 64 bit integer
- ffi.char
- Alias for
char
in C - ffi.uchar
- Alias for
unsigned char
in C - ffi.short
- Alias for
short
in C - ffi.ushort
- Alias for
unsigned short
in C - ffi.int
- Alias for
int
in C - ffi.uint
- Alias for
unsigned int
in C - ffi.long
- Alias for
int
in C - ffi.ulong
- Alias for
unsigned int
in C - ffi.longlong
- Alias for
long long
in C - ffi.ulonglong
- Alias for
unsigned long long
in C - ffi.size_t
- Alias for
size_t
in C - ffi.ssize_t
- Alias for
ssize_t
in C - ffi.float
- Floating point type:
float
in C - ffi.double
- Floating point type:
double
in C - ffi.longdouble
- Floating point type:
long double
in C - ffi.ptr[T=none]
- A pointer to another type. This is an example of a templated type. For example, in C you may have the type
typedef int* int_p;
(for a pointer to anint
). In this library, you can represent such a type with the expressionffi.ptr[ffi.int]
By default,
ffi.ptr
acts likeffi.ptr[none]
, which acts like avoid*
in C (i.e. pointer to anything)Pointer arithmetic is defined and adds the type of the size. For example:
>>> ffi.ptr[ffi.int](0x64) 0x64 >>> ffi.ptr[ffi.int](0x64) + 1 ffi.ptr[ffi.s32](0x68)
Also, dereferencing and assignment are supported through
[]
:>>> val = ffi.int(4) 4 >>> valp = ffi.addr(val) ffi.ptr[ffi.s32](0x557EE456A4F0) # Address of `val` >>> valp[0] = 5 5 >>> val # Now, that original value changed 5
- ffi.func[ResultT=none, ArgsT=()]
- A function type, with a result type and a list of argument types. This is an example of a templated type. For example, in C you may have the function type
int (*myfunc)(char x, short y)
. In this library, you can represent such a type with the expressionffi.func[ffi.int, (ffi.char, ffi.short)]
Variadic functions are supported via the
...
constant at the end of the arguments array. For example, theprintf
function in C has the typeffi.func[ffi.int, (ffi.ptr[ffi.char], ...)
.FFI functions can be called in kscript, just like a normal function. However, there are a few conversions that go on under-the-hood:
- Objects are casted to the function's signatures type (i.e. the template arguments)
- For variadic functions, extra arguments are expected to be either FFI types already, or a standard type (
int
,float
,str
,bytes
) that can be converted automatically str
andbytes
objects are converted into a *mutable*ffi.ptr[ffi.char]
. It is important that you don't pass them to a function which may mutate them! Use ffi.fromUTF8 and ffi.toUTF8 to convert them to mutable buffers
- ffi.struct[*members]
- A structure type, representing a
struct
type in C. This is an example of a templated type.Here are some examples:
""" C code struct vec3f { float x, y, z; }; """ # FFI code vec3f = ffi.struct[ ('x', ffi.float), ('y', ffi.float), ('z', ffi.float), ] # Sometimes, if you want to add customizability, you can create a new type: type vec3f extends ffi.struct[ (ffi.float, 'x'), (ffi.float, 'y'), (ffi.float, 'z'), ] { # You can even add methods! } # You can create values like this # vec3f(1, 2, 3) # vec3f(1, 2, 3).y == 2.0
- ffi.DLL()
- Represents a dynamically loaded library, typically opened via the ffi.open function.
- ffi.DLL.load(name, of=ffi.ptr)
- Loads a symbol from the DLL (by looking it up) which is expected to be of type
of
(default:void*
in C)For example, the
puts
function can be loaded like so:>>> libc.load('puts', ffi.func[ffi.int, (ffi.ptr[ffi.char],)])
See ffi.func
- ffi.open(src)
- Opens
src
, which is expected to be a string representing the dynamic library. For example,ffi.open('libc.so.6')
opens the C standard library on most systems.Returns an ffi.DLL
- ffi.sizeof(obj)
- Computes the size (in bytes) of an object (or, if given a type, the size of objects of that type)
- ffi.addr(obj)
- Returns the address of the value in
obj
, assuming its type is an FFI-based type. The address returns the address of the start of the actual C-value inobj
, which can be mutated and is only valid as long asobj
is still aliveReturn type is
ffi.ptr[type(obj)]
- ffi.fromUTF8(addr)
- Returns a string created from UTF8 bytes at
addr
(assumed to be NUL-terminated) - ffi.toUTF8(obj, addr=none, sz=-1)
- Converts
obj
(which is expected to be a string or bytes object) to UTF8 bytes, and stores inaddr
(default: allocate viamalloc()
)If
sz > 0
, then that is the maximum size of the buffer (including NUL-terminator), and no more thansz
bytes will be written - ffi.malloc(sz)
- Wrapper for the C function
malloc
- ffi.realloc(ptr, sz)
- Wrapper for the C function
realloc
- ffi.free(ptr)
- Wrapper for the C function
free
4. Syntax
- EBNF: Formal specification
- Expressions: Syntax elements yielding a result
- Statements: Syntax elements which do not yield a value
This section describes the syntax of the ksript language. It explains the actual formal specs of what is and what is not valid kscript code.
4.1. EBNF
EBNF is a notation to formalize computer grammars. In this page, the grammar of the kscript language is described using an EBNF-like syntax:
(* Entire program/file *)
PROG : STMT*
(* Newline/break rule *)
N : '\n'
| ';'
(* Block (enclosed in '{}') *)
B : '{' STMT* '}'
(* Block (B) or comma statement *)
BORC : B
| ',' STMT
(* Block (B) or statement *)
BORS : B
| STMT
(* Statement, which does not yield a value *)
STMT : 'import' NAME N
| 'ret' EXPR? N
| 'throw' EPXR? N
| 'break' N
| 'cont' N
| 'if' EXPR BORC ('elif' BORC)* ('else' EXPR BORS)?
| 'while' EXPR BORC ('else' EXPR BORS)?
| 'for' EXPR BORC
| 'try' BORS ('catch' EXPR (('as' | '->') EXPR)?)* ('finally' BORS)?
| EXPR N
| N
(* Expression, which does yield a value *)
EXPR : E0
(* Precedence rules *)
E0 : E1 '=' E0
| E1 '&=' E0
| E1 '^=' E0
| E1 '|=' E0
| E1 '<<=' E0
| E1 '>>=' E0
| E1 '+=' E0
| E1 '-=' E0
| E1 '*=' E0
| E1 '@=' E0
| E1 '/=' E0
| E1 '//=' E0
| E1 '%=' E0
| E1 '**=' E0
| E1
E1 : E2 'if' E2 ('else' E1)?
| E2
E2 : E2 '??' E3
| E3
E3 : E3 '||' E4
| E4
E4 : E4 '&&' E5
| E5
E5 : E5 '===' E6
| E5 '==' E6
| E5 '!=' E6
| E5 '<' E6
| E5 '<=' E6
| E5 '>' E6
| E5 '>=' E6
| E5 'in' E6
| E5 '!in' E6
| E6
E6 : E6 'as' E7
| E7
E7 : E7 '|' E8
| E8
E8 : E8 '^' E9
| E9
E9 : E9 '&' E10
| E10
E10 : E10 '<<' E11
| E10 '>>' E11
| E11
E11 : E11 '+' E12
| E11 '-' E12
| E12
E12 : E12 '*' E13
| E12 '@' E13
| E12 '/' E13
| E12 '//' E13
| E12 '%' E13
| E13
E13 : E14 '**' E13
| E14
E14 : '++' E14
| '--' E14
| '+' E14
| '-' E14
| '~' E14
| '!' E14
| '?' E14
| E15
E15 : ATOM
| '(' ')'
| '[' ']'
| '{' '}'
| '(' ELEM (',' ELEM)* ','? ')'
| '[' ELEM (',' ELEM)* ','? ']'
| '{' ELEM (',' ELEM)* ','? '}'
| '{' ELEMKV (',' ELEMKV)* ','? '}'
| 'func' NAME? ('(' (PAR (',' PAR)* ','?)? ')')? B (* Func constructor *)
| 'type' NAME? ('extends' EXPR)? B (* Type constructor *)
| 'enum' NAME? B (* Enum constructor *)
| E15 '.' NAME
| E15 '++'
| E15 '--'
| E15 '(' (ARG (',' ARG)*)? ','? ')'
| E15 '[' (ARG (',' ARG)*)? ','? ']'
(* Atomic element of grammar (expression which is single token) *)
ATOM : NAME
| STR
| REGEX
| INT
| FLOAT
| '...'
(* Valid argument to function call *)
ARG : '*' EXPR
| EXPR
(* Valid parameter to a function *)
PAR : '*' NAME
| NAME ('=' EXPR)?
(* Valid argument to container constructor (expression, or expand expression) *)
ELEM : '*' EXPR
| EXPR
(* Valid argument to key-val container constructor *)
ELEMKV : EXPR ':' EXPR
(* Tuple literal *)
TUPLE : '(' ','? ')'
| '(' ELEM (',' ELEM)* ','? ')'
(* List literal *)
LIST : '[' ','? ']'
| '[' ELEM (',' ELEM)* ','? ']'
(* Set literal (no empty set, since that conflicts with dict) *)
SET : '{' ELEM (',' ELEM)* ','? '}'
(* Dict literal *)
DICT : '{' ','? '}'
| '{' ELEMKV (',' ELEMKV)* ','? '}'
(* Function constructor *)
FUNC : 'func' NAME? ('(' (PAR (',' PAR)*)? ','? ')')? B
(* Type constructor *)
TYPE : 'type' NAME? ('extends' EXPR)? B
(* Enum constructor *)
ENUM : 'enum' NAME? B
(* Token kinds described as literals *)
NAME : ? unicode identifier ?
STR : ? string literal ?
REGEX : ? regex literal ?
INT : ? integer literal ?
FLOAT : ? floating point literal ?
4.2. Expressions
- Integer Literal: Whole number literal syntax
- Float Literal: Real number literal syntax
- Complex Literal: Complex literal syntax
- String Literal: String literal syntax
- List Literal: List literal syntax
- Tuple Literal: Tuple literal syntax
- Set Literal: Set literal syntax
- Dict Literal: Dict literal syntax
- Lambda Expression: Lambda expression syntax
- Function Definition: Function definition syntax
- Type Definition: Type definition syntax
- Operators: Binary and unary operators
In kscript, many syntax elements are expressions, which means that they will result in a value after being evaluated. You can always assign the result of an expression to a variable, element index, attribute, or any other destination where you may store in any object. They can be used within other expressions (albeit, sometimes requiring ()
due to order-of-operations).
In contrast to most languages, Function Definitions and Type Definitions are expressions (in most languages, they are statements and do not yield a value). You can, of course, use them like a statement (i.e. not embedded in another expression), but you can also return them, or assign them locally.
4.2.1. Integer Literal
This is the syntax for constructing literal integers (of type int). You can specify the base-10 digits within a kscript program, and it will be interpreted as an integer. You can also use a prefix for other notations -- see the table below for a list of valid ones:
- 0d
- Decimal notation involves the prefix
0d
or0d
or no prefix followed by a sequence of base-10 digits, which are one of:0
,1
,2
,3
,4
,5
,6
,7
,8
,9
- 0b
- Binary notation involves the prefix
0b
or0B
followed by a sequence of base-2 digits (called bits), which are either0
or1
- 0o
- Octal notation involves the prefix
0o
or0O
(that's "zero" "oh", as in the number and then letter) followed by a sequence of base-8 digits, which are one of:0
,1
,2
,3
,4
,5
,6
,7
. A common use of this notation is file permission bits, see: os.mkdir, os.stat - 0x
- Hexadecimal notation involves the prefix
0x
or0X
followed by a sequence of base-16 digits, which are are one of:0
,1
,2
,3
,4
,5
,6
,7
,8
,9
,a
/A
,b
/B
,c
/C
,d
/D
,e
/E
,f
/F
Regardless of the notation, the result is an int object with the specified value. Also, note that there are no "negative integer literals" -- only a positive one with a -
operator, which causes negation.
Examples:
>>> 123 # Base-10
123
>>> 255 # Base-10
255
>>> 0x7B # Base-16
123
>>> 0xFF # Base-16
255
>>> 0o173 # Base-8
123
>>> 0o377 # Base-8
255
>>> 0b1111011 # Base-2
123
>>> 0b11111111 # Base-2
255
4.2.2. Float Literal
This is the syntax for constructing literal floating point numbers (of type float). You can specify the base-10 digits within a kscript program, including a .
for the whole number/fractional seperator (which differentiates float literals from Integer Literal), and it will be interpreted as a real number, represented as accurately as the machine precision can (see float.EPS). Additionally, an exponent is allowed (see: scientific notation) with the e
or E
characters. You can also use a prefix for other notations -- see the table below for a list of valid ones:
- 0d
- Decimal notation involves the prefix
0d
or0d
or no prefix followed by a sequence of base-10 digits, which are one of:0
,1
,2
,3
,4
,5
,6
,7
,8
,9
- 0b
- Binary notation involves the prefix
0b
or0B
followed by a sequence of base-2 digits (called bits), which are either0
or1
. Must include a.
as a seperatorInstead of using
e
orE
for the base-10 power, you usep
orP
for a base-2 power. - 0o
- Octal notation involves the prefix
0o
or0O
(that's "zero" "oh", as in the number and then letter) followed by a sequence of base-8 digits, which are one of:0
,1
,2
,3
,4
,5
,6
,7
. Must include a.
as a seperatorInstead of using
e
orE
for the base-10 power, you usep
orP
for a base-2 power. - 0x
- Hexadecimal notation involves the prefix
0x
or0X
followed by a sequence of base-16 digits, which are are one of:0
,1
,2
,3
,4
,5
,6
,7
,8
,9
,a
/A
,b
/B
,c
/C
,d
/D
,e
/E
,f
/F
. Must include a.
as a seperatorInstead of using
e
orE
for the base-10 power, you usep
orP
for a base-2 power.
In addition to the digits, there are also two builtin names inf
and nan
, which represent positive infinity and not-a-number respectively.
Also, note that there are no "negative float literals" -- only a positive one with a -
operator, which causes negation.
Regardless of the notation, the result is a float object with the specified value.
Examples:
>>> 123.0 # Base-10
123.0
>>> 255.0 # Base-10
255.0
>>> 100.75 # Base-10
100.75
>>> 0x7B.0 # Base-16
123.0
>>> 0xFF.0 # Base-16
255.0
>>> 0x64.C # Base-16
100.75
>>> 0o173.0 # Base-8
123.0
>>> 0o377.0 # Base-8
255.0
>>> 0o144.6 # Base-8
100.75
>>> 0b1111011.0 # Base-2
123.0
>>> 0b11111111.0 # Base-2
255.0
>>> 0b1100100.11 # Base-2
100.75
Here are some examples with scientific notation:
>>> 1.234e3
1234
>>> 1234e-3
1.234
>>> 1e9
1000000000.0
4.2.3. Complex Literal
This is the syntax for constructing literal complex values. Specifically, you can only construct imaginary literals -- you need to use the +
operator to create a complex number with real and imaginary components.
Complex literals are created by placing an i
or I
directly after an Integer Literal or Float Literal, which results in a complex number with 0.0
as the real component, and the integer or floating point value as the imaginary component. Note that complex objects have both components as float values, so even though an integer imaginary component may be given (for example, 123i
), the resulting complex value will have floating point components.
Examples:
>>> 1i # Base-10
1.0i
>>> 123i # Base-10
123.0i
>>> 12+34i # Base-10
(12.0+34.0i)
>>> 0x1i # Base-16
1.0i
>>> 0x7Bi # Base-16
123.0i
>>> 0xC+0x22i # Base-16
(12.0+34.0i)
4.2.4. String Literal
This is the syntax for constructing literal str values. The basic syntax is a beginning quote character (one of '
, "
, '''
, """
), followed by the contents of the string, and then an ending quote character that matched the one the string started with. The contents of the string can be either character literals, or escape codes (see below for a list of escape sequences). The result will always be a str object, which is immutable.
Here's a list of escape sequences:
- \\\\
- A literal
\
- \'
- A literal
'
- \"
- A literal
"
- \a
- An ASCII
BEL
character (bell/alarm) - \b
- An ASCII
BS
character (backspace) - \f
- An ASCII
FF
character (formfeed) - \n
- An ASCII
LF
character (newline/linefeed) - \r
- An ASCII
CR
character (carriage return) - \t
- An ASCII
HT
character (horizontal tab) - \v
- An ASCII
VT
character (bertical tab) - \xXX
- Single byte, where
XX
are the 2 hexadecimal digits of the codepoint (padded with leading zeros)Examples:
>>> '\x61' 'a'
- \uXXXX
- Unicode character, where
XXXX
are the 4 hexadecimal digits of the codepoint (padded with leading zeros)Examples:
>>> '\u0061' 'a'
- \UXXXXXXXX
- Unicode character, where
XXXXXXXX
are the 8 hexadecimal digits of the codepoint (padded with leading zeros)Examples:
>>> '\U00000061' 'a'
- \N[XX...X]
- Unicode character, where
XX...X
is the name of the Unicode characterExamples:
>>> '\N[LATIN SMALL LETTER A]' 'a'
4.2.5. List Literal
This is the syntax for constructing literal list objects. A list is a mutable collection, so once the literal is created, it may be mutated further.
List literals are created by surrounding the elements of the list with [
and ]
, with ,
in between each element. Optionally, an additional ,
may be added after all of them. Items within a list literal may span across lines.
An empty list can be created with either []
or [,]
(they are equivalent).
List literals also support unpacking, which can be specified via a *
before an element in the literal. This instructs kscript to treat that element as an iterable, and instead of adding it, it adds each object within that element to the list.
List literals also support comprehension, which can be specified with the for
and optionally an if
keyword. You can use: a for b in c
(adds the expression a
for each element b
in an iterable c
) or a for b in c if d
(adds the expression a
for each element b
in an iterable c
, only if the expression d
is truthy). You can think of this as an inline for
loop.
Examples:
>>> []
[]
>>> [,]
[]
>>> [1, 2, 3]
[1, 2, 3]
>>> ["Any", "Type", "CanBeStored"]
['Any', 'Type', 'CanBeStored']
>>> [*"abcd"]
['a', 'b', 'c', 'd']
4.2.6. Tuple Literal
This is the syntax for constructing literal tuple objects. A tuple is a immutable collection, so once the literal is created, it may not be mutated further.
Tuple literals are created by surrounding the elements of the tuple with (
and )
, with ,
in between each element. Optionally, an additional ,
may be added after all of them. Items within a tuple literal may span across lines. Tuples with one element must end with a comma (i.e. (a,)
), to differentiate it from a grouping.
The empty tuple can be created via either ()
or (,)
Tuple literals also support unpacking, which can be specified via a *
before an element in the literal. This instructs kscript to treat that element as an iterable, and instead of adding it, it adds each object within that element to the tuple.
Tuple literals also support comprehension, which can be specified with the for
and optionally an if
keyword. You can use: a for b in c
(adds the expression a
for each element b
in an iterable c
) or a for b in c if d
(adds the expression a
for each element b
in an iterable c
, only if the expression d
is truthy). You can think of this as an inline for
loop.
Examples:
>>> ()
()
>>> (,)
()
>>> (1, 2, 3)
(1, 2, 3)
>>> ("Any", "Type", "CanBeStored")
('Any', 'Type', 'CanBeStored')
>>> (*"abcd")
('a', 'b', 'c', 'd')
4.2.7. Set Literal
This is the syntax for constructing literal set objects. A set is a mutable collection, so once the literal is created, it may be mutated further.
Set literals are created by surrounding the elements of the set with {
and }
, with ,
in between each element. Optionally, an additional ,
may be added after all of them. Items within a set literal may span across lines.
Due to a conflicts with Dict Literal, the empty set must be created via calling the set type: set()
Set literals also support unpacking, which can be specified via a *
before an element in the literal. This instructs kscript to treat that element as an iterable, and instead of adding it, it adds each object within that element to the set.
Set literals also support comprehension, which can be specified with the for
and optionally an if
keyword. You can use: a for b in c
(adds the expression a
for each element b
in an iterable c
) or a for b in c if d
(adds the expression a
for each element b
in an iterable c
, only if the expression d
is truthy). You can think of this as an inline for
loop.
Since set objects only store unique members (i.e. members which differ in hash and value), adding duplicate elements will result in a shorter set with fewer elements than intiailizers (see below in examples).
Examples:
>>> set()
set()
>>> {1, 2, 3}
{1, 2, 3}
>>> {"Any", "Type", "CanBeStored"}
{'Any', 'Type', 'CanBeStored'}
>>> {*"abcd"}
{'a', 'b', 'c', 'd'}
>>> {3, 2, 1, 2, 3} # Duplicate elements are not added
{3, 2, 1}
4.2.8. Dict Literal
This is the syntax for constructing literal dict objects. A dict is a mutable collection, so once the literal is created, it may be mutated further.
Dict literals are created by surrounding the key-value pairs of the dict with {
and }
, with :
seperating the key and value, and with ,
in between each element. Optionally, an additional ,
may be added after all of them. Items within a dict literal may span across lines.
The empty dictionary can be created via either {}
or {,}
.
Dict literals also support unpacking, however the syntax is not finalized yet. This is incomplete.
Dict literals also support comprehension, which can be specified with the for
and optionally an if
keyword. You can use: k: v for b in c
(adds the key-value entry k
and v
for each element b
in an iterable c
) or k: v for b in c if d
(adds the key-value entry k
and v
for each element b
in an iterable c
, only if the expression d
is truthy). You can think of this as an inline for
loop.
Since dict objects only store unique members (i.e. keys which differ in hash and value), adding duplicate elements will result in a shorter dict with fewer elements than intiailizers (see below in examples).
Examples:
>>> {}
{}
>>> {'a': 1, 'b': 2, 'c': 3}
{'a': 1, 'b': 2, 'c': 3}
>>> {'a': 1, 'b': 2, 'c': 3, 'a': 4} # Duplicate elements update the value, but do not add entries
{'a': 4, 'b': 2, 'c': 3}
4.2.9. Lambda Expression
This is the syntax for constructing func objects given a lambda expression. In some terminologies, lambda functions are called "anonymous functions", and to some extent they are, but there are also anonymous functions in func
expressions. So, "anonymous functions" refers to both lambda expressions and unnamed func
expressions. Throughout this section, we'll use the term "lambda expression" to refer to this syntax element.
Lambda expressions are created from a variable, or tuple
literal, followed by a right arrow (->
, made up of the characters -
and >
), and then followed by an expression. So, the basic form is a -> b
, where a
are the parameters, and b
is the expression to evaluate when the function is called (and it is returned from the function).
Further, the parameters may be assigned a default, but they must be within a tuple literal (for example, (x=1,) -> x
is valid, but x=1 -> x
, and (x=1) -> x
are invalid).
The main advantage of lambda expressions is that they are shorter than func
expressions, and do not require using the ret
statement to return a value.
Examples:
>>> x -> x + 2
<func '<lambda>(x)'>
>>> (x -> x ** 2)(3)
9
>>> foo = (x, y=2) -> x + y
<func '<lambda>(x, y=2)'>
>>> foo(10)
12
>>> foo(10, 4)
14
4.2.10. Function Definition
This is the syntax for constructing func objects with the func
keyword. It is important to note that in kscript (unlike many languages), function definition is an expression, not only a statement. So, it can be used within other expressions.
The basic syntax for defining a function begins with the func
keyword, optionally followed by a valid identifier as the name of the function, optionally followed by the parameters ((
, then all the arguments, then )
). Finally, it expects the body of the function within {
and }
Specifically, here are the rules of each part of the function definition:
- Name (optional)
- The name, which should be a valid identifier, tells what local name to assign to. If it is not given, then the function is not assigned to any name (called an "anonymous function")
- Parameters (optional)
- The parameters of the function, surrounded by
(
and)
, with,
in between each parameter. Each parameter may be a valid identifer, and you can writename=val
to provide a default argument. Additionally, you can specify spread parameters via*name
, which assigns the list of all extra arguments toname
Examples:
# Anonymous function, is not assigned to any named
# Takes 0 arguments
func {
# Body here
a()
b()
ret val
}
# Named function (it is assigned to the local name `foo`)
# Takes 0 arguments
func foo {
a()
b()
ret val
}
# Named function (it is assigned to the local name `foo`)
# Takes 2 arguments
func foo(x, y) {
ret x + y
}
# Named function (it is assigned to the local name `foo`)
# Takes 1 or 2 arguments (if only `x` is given, `y` defaults to `1`)
func foo(x, y=1) {
ret x + y
}
# Named function (it is assigned to the local name `foo`)
# Takes 1 or more arguments (`y` is a list of all other arguments given after the first)
func foo(x, *y) {
ret x + len(y)
}
# Named function (it is assigned to the local name `foo`)
# Takes 2 or more arguments (`y` is a list of all other arguments given after the first and before the last)
func foo(x, *y, z) {
ret x + len(y) + z
}
You can also create a lambda expression, which is a similar concept but different syntax (it is more compact).
4.2.11. Type Definition
This is the syntax for constructing type objects with the type
keyword. It is important to note that in kscript (unlike many languages), type definition is an expression, not only a statement. So, it can be used within other expressions.
The basic syntax for defining a type begins with the type
keyword, optionally followed by a valid identifier as the name of the type, optionally followed by the keyword extends
and an expression giving the base type (default: object). It should end with a block of statements beginning with {
and ending with }
Specifically, here are the rules of each part of the type definition:
- Name (optional)
- The name, which should be a valid identifier, tells what local name to assign to. If it is not given, then the type is not assigned to any name (called an "anonymous type")
- Extends (optional)
- The base type of the type being created is specified by giving the
extends
keyword and then an expression that will yield the base type. Default is object
Examples:
# Anonymous type
type {
...
}
# Named type
type MyType {
...
}
# Named type which is a subtype of `list`
type MyType extends list {
...
}
4.2.12. Operators
Operators are used to represent operations on other expressions (called "operands"). There are a few different kind of operators, which are explained briefly below:
- Unary operators (such as
+x
,-x
, and so on), which take a 1 operand - Binary operators (such as
x+y
,x-y
, and so on), which take 2 operands - Ternary operators (such as
x if y else z
), which take 3 operands
Within binary operators, there are also associativity. For example, a + b + c
is parsed as (a + b) + c
(left associativity), but a = b = c
is parsed as a = (b = c)
(right associativity). Unless explicitly stated, operators are assumed to be left-associative.
Operators in kscript also are divided into precedence levels. For example, a + b + c
is parsed as (a + b) + c
, but a + b * c
is parsed as a + (b * c)
.
Here is a list of the operators in order of precedence (lowest first):
=, &=, ^=, |=, <<=, >>=, +=, -=, *=, @@=, /=, //=, %=, **=
if/else
??
||
&&
===, ==, !=, >, <=, >, >=, in, !in
|
^
&
<<, >>
+, -
*, @@, /, //, %
**
~, !, ++ (pre), -- (pre), + (unary), - (unary)
., ++ (post), -- (post)
Here are the operators in order of precedence (lowest first) (you can see the syntax of kscript to see them as well):
- =, &=, ^=, |=, <<=, >>=, +=, -=, *=, @=, /=, //=, %=, **=
- All right associative assignment operators. The base assignment operator (
=
) assigns the right side to the left side.Other operators take the left and right side, apply the operator before the
=
character, and then assign that to the left side. - if/else
- The
if/else
operator (the ternary operator) is kind of special -- you write the expression asa if b else c
(ora if b
, withc
defaulting tonone
). It is an inline version of theif
statement - ??
- The
??
operator is the short-circuiting and exception-catching operator, which first evaluates the left-operand, and if it was successful, yields that value and short-circuits (does not evaluate the right-operand). Otherwise, if an Exception was thrown while executing it, then the right-operand is evaluated and used as the result. These can be chained. For example,x ?? y ?? z
first evaluatedx
, and if an exception is encoutered, it is caught and theny
is evaluated. If an exception is also thrown iny
, thenz
is evaluated. If an exception is thrown inz
, then it is not caught - ||
- The
||
operator is the short-circuiting inclusive-or operator, which first evaluates its left-operand, and if it is truthy (see bool), then it is used as the result. Otherwise, the right-operand is evaluated and its result is used - &&
- The
&&
operator is the short-circuiting and operator, which first evaluates the left-operand, and if it is truthy (see bool), then its right-operand is evaluated and use as the result. Otherwise, the left-operand is used as the result - ==
- The comparison operator representing equality (may be chained with other comparison operators to create a rich comparison).
May be defined via the __eq attribute
- !=
- The comparison operator representing inequality (may be chained with other comparison operators to create a rich comparison).
May be defined via the __ne attribute
- <
- The comparison operator representing less-than (may be chained with other comparison operators to create a rich comparison).
May be defined via the __lt attribute
- <=
- The comparison operator representing less-than-or-equals (may be chained with other comparison operators to create a rich comparison).
May be defined via the __le attribute
- >
- The comparison operator representing greater-than (may be chained with other comparison operators to create a rich comparison).
May be defined via the __gt attribute
- >=
- The comparison operator representing greater-than-or-equals (may be chained with other comparison operators to create a rich comparison).
May be defined via the __ge attribute
- in
- The contains operator representing whether the left-operand is an element in the right-operand
May be defined via the __contains attribute
- !in
- The not-contains operator representing whether the left-operand is not an element in the right-operand
May be defined via the __contains attribute
- as
- The function call operator, which calls the right-operand with the left-operand as the only argument. Mainly used as syntactic sugar for type conversions.
For example,
2.5 as int
is the same asint(2.5)
- |
- Represents a bitwise inclusive
or
of the left-operand and right-operand. Can be overriden via the __ior attribute. - ^
- Represents a bitwise exclusive
xor
of the left-operand and right-operand. Can be overriden via the __xor attribute. - &
- Represents a bitwise
and
of the left-operand and right-operand. Can be overriden via the __and attribute. - <<
- Represents a left shift of the left-operand and right-operand. Can be overriden via the __lsh attribute
- >>
- Represents a right shift of the left-operand and right-operand. Can be overriden via the __rsh attribute
- +
- Represents a addition of the left-operand and right-operand. Can be overriden via the __add attribute
- -
- Represents a subtraction of the left-operand and right-operand. Can be overriden via the __sub attribute
- *
- Represents a multiplication of the left-operand and right-operand. Can be overriden via the __mul attribute
- @
- Represents a matrix multiplication of the left-operand and right-operand. Can be overriden via the __matmul attribute
- /
- Represents a division of the left-operand and right-operand. Can be overriden via the __div attribute
- //
- Represents a floored division of the left-operand and right-operand. Can be overriden via the __floordiv attribute
- %
- Represents a modulo of the left-operand and right-operand. Can be overriden via the __mod attribute
- **
- Represents a power of the left-operand and right-operand. Can be overriden via the __pow attribute
- ~
- Represents a conjugation of the operand. Can be overriden via the __sqig attribute
- + (unary)
- Represents an identity operation of the operand. Can be overriden via the __pos attribute
- - (unary)
- Represents a negation of the operand. Can be overriden via the __pos attribute
- !
- Represents a negation of the truth value (bool) of the operand
4.3. Statements
- Expression Statement: Executes an expression
- Assert Statement: Validating an assertion
- Cont Statement: Continue through a loop
- Break Statement: Break a loop
- Ret Statement: Returning a value from a function
- Throw Statement: Throwing an exception up the call stack
- If Statement: Conditional code execution
- While Statement: Conditional loop execution
- For Statement: Iterable loop execution
- Try Statement: Exception handling construct
- Import Statement: Module importing
These elements do not yield a value, and are typically used for control flow.
In contrast to most languages, Function Definitions and Type Definitions are expressions (in most languages, they are statements and do not yield a value). You can, of course, use them like a statement (i.e. not embedded in another expression), but you can also return them, or assign them locally.
4.3.1. Expression Statement
The expression statement is an expression, which has a line break after it, or, equivalently, a semicolon (;
). You can write code without using semicolons (;
), and it is recommended to not use them. For example, calling foo()
and bar()
can be done in a few ways:
# This is the best, it is clear and readable
foo()
bar()
# Don't do this, the ';' are unnecessary
foo();
bar();
# Don't do this, it's less readable
foo(); bar()
However, if there is some circumstance that makes it better to have multiple on the same line, you may have expression statements ended with ;
. This is sometimes useful when creating one liners running with the interpreter, but in production code or code you are sharing with anyone else, you should just put them on seperate lines.
4.3.2. Assert Statement
The assert
statement is an example of assertion syntax for the kscrip language. It instructs the program to evaluate a conditional expression, check whether it is truthy (see bool), and if it is not truthy, throws an AssertError up the call stack.
This is done to do "sanity checks" on things which should be a given value, and is encouraged for only internal code checking (i.e. don't check user input with assert
, use if
and then throw
an Exception yourself). Here is the syntax:
# Evaluates `x` and asserts that it is true
assert x
4.3.3. Cont Statement
The cont
statement prematurely terminates the innermost loop (which may be a while
statement, or for
statement) and retries the next iteration.
As a result, it must be placed within a while
or for
statement. For example:
# infinite loop
while true {
# Stops executing now, and continues the loop
cont
# This code never runs
x = 3
}
See also the Break Statement
4.3.4. Break Statement
The break
statement prematurely terminates the innermost loop (which may be a while
statement, or for
statement) and then stops executing the loop
As a result, it must be placed within a while
or for
statement. For example:
# looks like an infinite loop, but is not since the `break` statement will terminate it
while true {
# Stops executing now, and exits the loop
break
# This code never runs
x = 3
}
See also the Cont Statement
4.3.5. Ret Statement
The ret
statement is an example of return syntax for the kscript language. It instructs the program to return a value (or none
) from the current function, and stop executing in that function.
For example:
# Returns the value `a` and stops executing the current function
ret a
# Equivalent to `ret none`
ret
4.3.6. Throw Statement
The throw
statement is an example of exception handling syntax for the kscript language. It instructs the program to throw a value up the call stack, which searches through the current functions executing in the thread, until it finds a try
statement. If it does find a try
statement, then it is handled to rules according to that syntax. If there were no try
statements that catch the exception, then the program is halted and the exception is printed (this is called an "unhandled exception").
For example:
# Throws the value `a` up the call stack
throw a
To see how it can work, look at this small example:
# Innocent enough looking function...
func foo(x) {
# Some error condition
if x == 0 {
throw Exception("'x' should never be 0")
}
# Do something else
...
}
# Fine
foo(1)
# Fine
foo(2)
# Uh-oh, this crashes and prints: Exception: 'x' should never be 0
# (and, at this point, the program stops)
foo(0)
# Now, wrap in a `try` statement
try {
foo(0)
} catch as err {
# This works, and it prints what it caught ("I got: Exception")
# It doesn't stop the program
print ("I got:", type(err))
}
4.3.7. If Statement
The if
statement is an example of a conditional statement, which evaluates a condition, and then based on the result (specifically, whether it was truthy or falsey), either runs another statement, or does not. An if
statement has the following syntax:
if a {
b
}
Where a
is an expression, and treated as the conditional. If a
is truthy (see bool), then b
is executed, which can be zero-or-more statements. After the closing }
, there may be additional clauses.
Specifically, there is the elif
clause:
if a {
b
} elif c {
d
} elif e {
f
}
c
is only executed if a
was not truthy. If c
was truthy, then d
is executed. Similarly, if c
was not truthy, then e
is evaluated, and if it is truthy then f
is executed. You can stack as many elif
clauses as you want.
There is also the else
clause, which is given after the if
and elif
parts (it must be the last). The syntax is:
if a {
b
} elif c {
d
} elif e {
f
} else {
g
}
g
is only executed if a
, c
, and e
were all falsey.
Sometimes, it is advantageous or more consise to have an if
statement without using the {
and }
surrounding the body. If the body only has a single statement, then you can use this syntax instead:
if a, b
Which is equivalent to:
if a {
b
}
As long as b
is only a single statement.
It is always recommended to keep an inline if
on a single line. For example, do not do the following:
# THIS IS BAD. DO NOT DO
if a,
b
This is confusing, because the indentation suggests there is another block, and if you had written:
# THIS IS BAD. DO NOT DO
if a,
b
c
Then a reader may think that b
and c
are both executed if a
is true, but c
is actually executed regardless!
4.3.8. While Statement
The while
statement is an example of a conditional loop, which evaluates a condition, and then based on the result (specifically, whether it was truthy or falsey), either runs another statement and then retries the condition, or exits the loop. An while
statement has the following syntax:
while a {
b
}
Which first evaluates a
. If a
is truthy (see bool), then the body, b
, is executed (which is zero-or-more statements), and then this process is repeated, re-evaluating a
and then re-executing b
if it is truthy. If a
is falsey, then the loop is exited.
Similar to an if
statement, while
statements allow for additional elif
clauses and a final else
clause. The elif
and else
clauses are only checked and executed if the conditional, a
, was falsey on the first time through the loop. So, if a
was truthy, and then falsey, then none of the elif
or else
clauses are checked.
Let's take an example:
while a {
b
} elif c {
d
} else {
e
}
When encountering the while
, a
is first evaluated. While it is truthy, b
is executed, and a
is rechecked, and b
is executed again, until a
is falsey. If a
was never truthy (which means b
is not executed at all), then c
is evaluated. If c
is truthy, then d
is executed. Otherwise, if c
is falsey, e
is executed.
Sometimes, it is advantageous or more consise to have an while
statement without using the {
and }
surrounding the body. If the body only has a single statement, then you can use this syntax instead:
while a, b
Which is equivalent to:
while a {
b
}
As long as b
is only a single statement.
It is always recommended to keep an inline while
on a single line. For example, do not do the following:
# THIS IS BAD. DO NOT DO
while a,
b
This is confusing, because the indentation suggests there is another block, and if you had written:
# THIS IS BAD. DO NOT DO
while a,
b
c
Then a reader may think that b
and c
are both executed if a
is true, but c
is actually executed regardless!
4.3.9. For Statement
The for
statement is an iterator-based for
loop, which is sometimes called a foreach
loop. It iterates over elements of an iterable. Specifically, you can write a for
loop like so:
for a in b {
c
}
Which first evaluates b
, and treats it as an iterable (see iter()). It is iterated over via the next() function, until the iterable runs out. For each element, it is assigned to a
(which must be an assignable expression), and then the body, c
(which may be zero-or-more statements), is ran.
Similar to an if
statement, for
statements allow for additional elif
clauses and a final else
clause. The elif
and else
clauses are only checked and executed if the iterable, b
, was empty on the first time through the loop. So, if b
had any elements, and then runs out, then none of the elif
or else
clauses are checked.
Let's take an example:
for a in b {
c
} elif d {
e
} else {
f
}
When encountering the for
, b
is first evaluated and turned into an iterable via the iter() function. While there is another element (by calling the next() function), it is assigned to a
(which should be an assignable expression). Then, the code block c
is executed (which is expected to be zero-or-more statements). If it was empty (i.e. c
was never ran, and a
was never assigned to), then it goes through the elif
clauses. In this case, it evaluates d
and checks whether it was truthy. If it was, then e
is executed. Otherwise, f
is executed.
Sometimes, it is advantageous or more consise to have an for
statement without using the {
and }
surrounding the body. If the body only has a single statement, then you can use this syntax instead:
for a in b, c
Which is equivalent to:
for a in b {
c
}
As long as b
is only a single statement.
It is always recommended to keep an inline for
on a single line. For example, do not do the following:
# THIS IS BAD. DO NOT DO
for a in b,
c
This is confusing, because the indentation suggests there is another block, and if you had written:
# THIS IS BAD. DO NOT DO
for a in b,
c
d
Then a reader may think that b
and c
are both executed if a
is true, but c
is actually executed regardless!
4.3.10. Try Statement
The try
statement (also called try/catch
statement) is the exception handling syntax for kscript. It allows exceptions thrown with the throw
statement while executing the body of the try
statement to be "caught", and then handled appropriately.
There are a few variations of the try
statement. Specifically, the catch
clauses may differ. All try
statements begin with the format:
try {
a
}
Here, a
is the "body" of the try
statement. Then, like elif
and else
clauses in the if
statement, there may be any number of catch
clauses, and then, optionally, a finally
clause as well. Also, like elif
and else
clauses in an if
statement, only one body of the catch
clause can be ran. It checks the catch
clauses sequentially, and the first type specifier that is a match (or, if the type specifier was ommitted, any exception matches) it runs the corresponding block, and does not check subsequent catch
clauses
Here are the variations of the catch
clause:
try {
a
} catch NameError {
# This is only selected if the exception thrown was a subtype of `NameError`
} catch NameError as err {
# This is only ran if the exception thrown was a subtype of `NameError`, and captures
# the thrown object as the local name `err`
print (err)
} catch (NameError, SizeError) as err {
# This is only selected if the exception thrown was a subtype of `NameError` or `SizeError`, and
# captures the thrown object as the local name `err`
# You can include however many elements in the tuple as you want, this will match an exception
# which is a subtype of any of the elements within the tuple
} catch {
# This is selected for any thrown exception
} catch as err {
# This is selected for any thrown exception, and captures the thrown object as the local name `err`
}
And, after all the catch
clauses, there is also, optionally, a finally
clause which is allowed:
try {
a
} catch {
...
} finally {
b
}
b
is executed whether an exception was thrown, or handled, or not. It is always ran after a
, and after a catch block (if one is ran at all). These are normally used to close resources which need to be closed whether the exception occurs or not. And, it allows the developer to put shared code here instead of in each catch
clause.
Just to explain further:
- If
a
is executed and no exception occurs, it then runsb
- If
a
is executed an exception occurs, and is handled in acatch
clause, it then runsb
- If
a
is executed and an exception occurs, and is not handled in acatch
clause, it then runsb
, and then re-throws the exception, going up the call stack finding the nexttry
statement, or causing the program to halt and print the unhandled exception
4.3.11. Import Statement
To import a module (for example, one of the builtin modules), you can use the import
statement. If there was an error finding or importing the module, an ImportError is thrown.
The statement begins with the import
keyword, followed by a valid identifier.
Examples:
# imports the 'os' module
import os
# imports the 'net' module
import net