đ The Charly Interpreter
Your syntax files need to be encoded in UTF-8.
Single-line comment start with //
. Multiline comments are created via /*
and */
// I'm a comment
/*
I'm a multiline comment
*/
Variables are defined via the let
keyword.
If you try to access a variable that wasnât initialized before, a runtime exception is thrown.
let myVar = 25
let myOtherVariable = 50
myVar + myOtherVariable // => 75
Constants are declared the same way as variables. They canât be changed, hence the name constant. You need to initialize them in the declaration.
const myconst // => syntax error
const myOtherConst = 25
myOtherConst = 30 // => runtime exception
Charly has a total of 10 primitive types. Not all can be constructed directly.
For example the TInternalFunction
can only be gathered from a call to __internal__method
Literal | Type |
---|---|
25 | Numeric |
25.5 | Numeric |
âCharlyâ | String |
[1, 2, 3] | Array |
null | Null |
NAN | Numeric |
false | Boolean |
true | Boolean |
class Box {} | Class |
func() {} | Function |
->() {} | Function |
{ let name = âCharlyâ } | Object |
You can get the type of a value by using the
typeof
operator. It returns the name of the type as a string.
typeof 25 // => "Numeric"
typeof "hello world" // => "String"
typeof null // => "Null"
typeof func() {} // => "Function"
The Null
type can be compared to undefined
in javascript or nil
in some other languages.
null
A Boolean
only has two different values: true
and false
.
true
false
All Numeric types inside Charly are Crystalâs native Float64
type.
Numeric literals, just like any other expression in the language, can be prefixed with -
to negate them.
Underscores can be used to make some numbers more readable:
1_000_000 // better than 1000000
Floats are created using the .
character.
123_456.456_789
A Numeric will silently overflow if you pass the lower or upper limit of Float64
.
The following REPL session elaborates this:
Operations such as 1 / 0
will result in the NAN
value.
A String represents an immutable sequence of UTF-8 characters.
It uses Crystalâs native String
type underneath.
You can create it using "
characters.
"hello world"
A backslash can be used to denote various special characters inside the string:
"\"" // double quote
"\\" // backslash
"\e" // escape
"\f" // form feed
"\n" // newline
"\r" // carriage return
"\t" // tab
"\v" // vertical tab
A string can span multiple lines:
"hello
world" // same as "hello\n world"
An Array is a resizeable list of items of any type. It is typically created with an array literal:
[1, 2, 3]
[1, "hello world", ["whats up"]]
You can add new items to an array using the push method:
let nums = []
nums.push(0)
nums.push(1)
nums.push(2)
nums // => [0, 1, 2]
You can concat two arrays together via the +
operator:
[1, 2] + [3, 4] // => [1, 2, 3, 4]
You can compare two arrays using the ==
operator:
[1, 2, 3, 4] == [1, 2, 3, 4] // => true
[1, 2] == [3, 4] // => false
Charly doesnât have special syntax to create objects. Instead it uses something we call Containers
.
A Container
is basically the scope of a block turned into an Object:
let Box = {
let name = "Charly"
let age = 200
}
Box.name // => "Charly"
Box.age // => 200
This can be compared to the javascript equivalent of using new Function()
:
let Box = (new function() {
this.name = "Charly"
this.age = 200
})
Box.name // "Charly"
Box.age // 200
You can access properties of objects via []
:
let Box = {
let name = "mybox"
}
Box["name"] // => "mybox"
You can define a new function like this:
func foo() {
return "hello world"
}
The last expression inside a function is used as itâs return value
func foo() {
25
}
foo() // => 25
You can also do explicit returns
func foo() {
if true {
return true
}
return false
}
foo() // => true
If you need the function literal, you can use anonymous function literals:
func foo(callback) {
callback(42)
}
foo(func(arg) {
print(arg) // => 42
})
There is also the lambda syntax, which goes like this:
[1, 2, 3].map(->(num) {
print(num)
})
[1, 2, 3].map(->(num) print(num))
Of course you could also pass the print method directly, this would however result in the following:
[1, 2, 3].map(print)
/*
1
0
3
2
1
3
3
2
3
*/
Because Array#map
passes the value, index and size of the array to the callback,
print will write all these values to the console.
When you write a lambda function without parenthesis or curly braces, it will wrap the expression inside a block.
The following:
foo(->25)
becomes:
foo(->{ 25 })
which in turn getâs converted to:
foo(func() {
return 25
})
All arguments are also inserted via a quick access identifier.
[1, 2, 3].map(->$0 * 2) // => [2, 4, 6]
Classes in Charly can inherit from multiple other classes.
They can have instance methods and properties and also static methods and properties.
Below is an example of a simple Person
class.
class Person {
property name
property age
property height
func constructor(name, age, height) {
@name = name
@age = age
@height = height
}
func greet() {
print("My name is " + @name)
print("I am " + @age + " years old")
print("I am " + @height + " meters tall")
}
}
let John = Person("John", 21, 1.85)
John.greet()
/*
Will print:
My name is John
I am 21 years old
I am 1.85 meters tall
*/
You define properties via the property
keyword followed by an identifier.
To define a static method or property, prefix the property or func keyword with the static
keyword.
class Box {
static property count
static func foo() {
"class method"
}
}
Box.count = 0
Box.foo() // => "class method"
To inherit from other classes, you use the extends
keyword.
class Foo {
func foo() {
"foo method"
}
}
class Bar {
func bar() {
"bar method"
}
}
class Baz extends Foo, Bar {
func baz() {
"baz method"
}
}
let myBaz = Baz()
myBaz.foo() // => "foo method"
myBaz.bar() // => "bar method"
myBaz.baz() // => "baz method"
Static properties and methods are also copied to the child classes. The values of static properties are copied by value. They are not references.
class Foo {
static property foo
static func what() {
"static what"
}
}
Foo.foo = "test"
class Bar extends Foo {}
Bar.what() // => "static what"
Bar.foo // => "test"
Foo.foo = "hello world"
Bar.foo // => "test"
@test
will be rewritten to self.test
by the parser automatically.
The self
reference always points to the object a method was called on.
let myBox = {
let name;
func foo() {
@name
}
}
myBox.name = "box"
myBox.foo() // => "box"
If you directly call a method foo()
, self
is set to whatever it what in the context where the method is defined.
Think of it like Arrow Funtions in JavaScript.
let Box = {
let name = "box"
func foo() {
return func() {
print(self.name)
}
}
}
let method = Box.foo()
method() // => "box"
Assignment is done with the =
character.
// assigns to a local variable
local = 1
// assigns to the current self variable
@instance = 2
// The above is simply rewritten to
self.instance = 2
All control expressions inside Charly behave as if they were normal expressions.
This means that altough you canât place a control expression inside a method call, it will still return a value.
func foo(arg) {
if arg < 25 {
true
} else {
false
}
}
foo(10) // => true
foo(40) // => false
A truthy value is a value that is considered true for an if
and while
guard.
A falsey value is a value that is considered false in those places.
The only falsey values are false
and null
. Any other value is truthy.
The parenthesis around the test expression are optional
if 2 < 5 {
print("Mathematics still works!")
} else {
print("Something's off...")
}
if 2 + 2 == 9 - 5 {
print("Mathematics still works!")
} else {
print("Somethings's off...")
}
For statements which require a value to be falsey, you can use the unless
statement.
unless 2 < 5 {
print("Something's off...")
}
unless obj.failed() {
print("Everything worked!")
}
Additionally, there is a guard
statement.
let input = "enter your name> ".prompt()
guard input.length() > 0 {
print("You didn't type anything.")
}
guard false {
// This block will run
}
For more complex branching, you can use the switch
statement.
let num = 25
switch num {
case 1, 2, 3 {
print("num is either 1, 2 or 3")
}
case 25 {
print("num is 25")
}
default {
print("num is neither 1, 2, 3 or 25")
}
}
The switch statement can also be inlined.
let num = 25
let answer = switch num {
case 20 {
"num is 20"
}
default {
"num is not 25"
}
}
The switch statement yields the value of the case statement which was run.
func foo(num) {
switch num {
case 20 {
"the argument was 20"
}
default {
"the argument wasn't 20"
}
}
}
foo(20) // => "the argument was 20"
foo(25) // => "the argument wasn't 20"
foo(30) // => "the argument wasn't 20"
The parenthesis around the test expression are optional
while true {
print("and another one")
}
let i = 0
while i < 100 {
print(i)
i += 1
}
For simple loops that repeat for a fixed amount of time, you are encouraged to use the Numeric#times
method.
5.times(->(i){
print(i)
})
5.times(->{
print("hello world")
})
You can break inside a while statement.
let i = 0
while true {
if i >= 100 {
break
}
print(i)
i += 1
}
continue
is also supported.
let i = 0
while i < 50 {
if i % 2 == 0 {
i += 1
continue
}
print(i)
i += 1
}
A inverted method of the while loop also exists. The until
loop runs while the test returns false.
let i = 0
until i == 100 {
i += 1
}
i // => 100
To create infinite loops, you can use the loop
statement.
loop {
print("hello world")
}
The next sections will assume you know what object oriented programming is, as well as the concepts of classes and methods.
Everything in Charly is an object. Not every type can have an internal state however.
Only Object
, Class
, PrimitiveClass
and Array
can have an internal state.
When you write 5
, the interpreter actually treats it as a primitive.
There are no funny castings or object instantiations (inside Charly). When you write 5.times
,
the interpreter searches for a primitive class called Numeric
and checks if it contains a method called times
.
This allows the interpreter to reuse the same object for all primitives of the same type.
This principle applies to all language primitives.
The primitive class Array
for example, specifies a method called push
which inserts an element into the array.
If a method expects to be called with 2 arguments, you have to pass two. If you pass 1 it throws an exception.
func foo(a, b, c) {
return true
}
foo(1, 2, 3) // => true
foo(1, 2, 3, 4) // => true
foo(1, 2) // => runtime exception
+ // positive, doesn't do anything right now
- // negative
! // not
~ // bitwise NOT
+ // addition
- // subtraction
* // multiplication
/ // division
% // modulo
& // bitwise and
| // bitwise or
^ // bitwise xor
** // exponentiation
<< // shift left, append
>> // shift right
== // equals
! // not equals
< // less
<= // less or equal
> // greater
>= // greater or equal
+= // left = left + right
-= // left = left + right
*= // left = left + right
/= // left = left + right
%= // left = left + right
**= // left = left + right
[] // array index and object property access
You can throw exceptions from anywhere in the program. Everything is throwable.
func foo(arg) {
if arg < 10 {
throw Exception("arg is smaller than 10")
}
}
foo(5)
This will show up in the console as:
test/debug.ch
1. func foo(arg) {
2. if arg < 10 {
-> 3. throw Exception("arg is smaller than 10")
4. }
5. }
at debug.ch:3:5:5
at foo (debug.ch:7:1:3)
Uncaught Object:Exception: arg is smaller than 10
You can include other files using the require
method. It accepts a single string argument that serves as the filename.
When including a file, the contents of the export
variable is then returned by the require
call.
Example:
main.ch
let external = require("./external.ch")
print(external.message) // "hello world"
print(external.foo(1, 2)) // 3
external.ch
export = {
let message = "hello world"
func foo(l, r) {
l + r
}
}
If you call require on the same file twice, it will result the value returned by the very first call.
If you call require on a file that was already required before, a cached version will be returned. The file wonât be executed twice.
main.ch
let external = require("./external.ch")
external.message = "it changed"
let external_second = require("./external.ch")
print(external_second.message) // => "it changed"
external == external_second // => true
external.ch
export = {
let message = "hello world"
}
Recursive require
call wonât be catched or prevented in any way.
The Charly
command has the ability to receive flags.
A list of these can be obtained by running charly -h
in the command line.
You can pass flags via the following format
charly input.ch -f lint
Arguments which are not flags are passed to the program instead of the interpreter.
charly input.ch hello -f tokens world 25 25 --foo -b
^ ^ ^ ^ ^ ^ ^ ^
| |__| |_____|__|__|_____|
ARGV | |
IFLAGS ARGV
You can access command line arguments and flags via the ARGV
and IFLAGS
constants.
Current environment variables are available via ENV
.
charly input.ch hello -f tokens world 25 25 --foo -b
ARGV // => ["hello", "world", 25, 25, "--foo", "-b"]
IFLAGS // => ["tokens"]
ENV["TERM"] // => xterm-256color
You can see the license and a list of contributors via the following commands
$ charly --license
The MIT License (MIT)
Copyright (c) 2016 Leonard Schuetz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# Contributors
- [Leonard SchĂŒtz (Author) @KCreate](https://github.com/KCreate)
- ...
Charly has a built-in REPL (Read Eval Print Loop) where you can quickly test out new ideas.
You can invoke it by typing charly
or charly repl
Here you can write Charly statements. Run them by pressing enter.
The REPL provides a few magic variables which are specific to your current REPL session.
$
This variable always contains the value of the last evaluated expressionhistory
This is an array of all commands you have enteredcontext
This is the top-level context of the REPL.echo
Boolean value to toggle the echo of entered expressionsPrimitives in Charly can be extended as if they were regular objects.
A good example of this is the Numeric#times
method. It allows you to write really expressive code like this:
5.times(->{
print("Hello!")
})
It is implemented like this:
Numeric.methods.times = func(callback) {
let i = 0
while (i < self) {
callback(i)
i += 1
}
self
}
You can add your own methods to primitive classes via the Numeric.methods
object.
For arrays you would use Array.methods
, for strings String.methods
and so on.
Letâs define a indent
method on strings which takes two arguments, the amount and a filler string.
String.methods.indent = ->(amount, filler) {
@split("\n").each(->(line) {
(value * amount) + line
}).join("\n")
}
You can now indent strings via the String#indent
method.
"hello\nworld\nwhats\nup".indent(2, "-")
/*
--hello
--world
--whats
--up
*/
Charly provides a built-in unit-testing library. Create a file containing the code below and update the testcases array with the paths to your test files.
You can then run the test cases with this command: charly main.ch
.
const UnitTest = require("unit-test")
const result = UnitTest(->(describe, it, assert, context) {
const testcases = [
["My test suite", "./mytest.ch"],
["My other test suite", "./myothertest.ch"]
]
// Loads and runs all the test cases sequentially
testcases.each(->(test) {
const module = require(test[1])
describe(test[0], ->{
module(describe, it, assert, context)
})
})
})
UnitTest.display_result(result, ->(code) {
io.exit(code)
})
Running this file will output the following:
$ charly main.ch
Charly [4f4e1bc] Unit Testing Framework
.FFFF.......
Some test suites have failed
1) some behaviour does something else
Assertion #1
Expected: false
Got: true
2) some behaviour some more extended test cases does something
Assertion #1
Expected: 4
Got: 6
You can also run a single file directly with the following command: charly test mytest.ch
Every file exporting a function taking three arguments can be run as a test suite. The only pre-requisite is that it follows the schema below:
mytest.ch
export = ->(describe, it, assert) {
describe("some behaviour", ->{
it("should do something", ->{
assert(25 + 25, 50)
assert(true, true)
assert("hello", "hello")
})
it("does something else", ->{
assert(true, false)
})
describe("some more extended test cases", ->{
it("does something", ->{
assert(2 + 4, 4)
})
})
})
}
Things related to IO and the file system are either directly included in the prelude, or
are available via the fs
module. The prelude contains bindings to STDIN, STDOUT and STDERR
and also provides some convenience functions to access these files.
// newline terminated string to STDOUT
print("hello world")
// non-terminated string to STDOUT
write("hello world")
// read a line from STDIN
// the first argument is the prompt
// the second argument is wether the input should be appended to the history
// this uses libreadline internally
gets("enter: ", false)
// read a single char from STDIN
getc()
Access to the file system is provided via the fs
module.
const fs = require("fs")
let file = fs.open("foo.txt", "w+", "utf8")
file.puts("hello world")
file.close()
// foo.txt
hello world\n
See fs.ch for a list of available methods and their documentation.
Charly currently has rudimentary support for native extensions written in Crystal.
The way this works is via Crystal files that have to be compiled into the interpreter itself.
You can add your own files like this:
Create a file called myfile.cr
inside src/charly/interpreter/internals
Insert the following code:
require "../**"
module Charly::Internals
charly_api "mymethod", TString do |myarg|
return TString.new("You said: " + myarg.value)
end
end
Recompile & reinstall the interpreter
Link against the method inside your Charly program like this:
const mymethod = __internal__method("mymethod")
print(mymethod("Hello World")) // => You said: Hello World