├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── async-files-ls.kal ├── async-http-client.kal ├── async-http-server.kal ├── conditionals.kal ├── lists-and-comprehensions.kal └── literate.litkal ├── package.json ├── scripts └── kal ├── source ├── ast.litkal ├── command.litkal ├── generator.litkal ├── grammar.litkal ├── interactive.litkal ├── kal.litkal ├── lexer.litkal ├── literate.litkal ├── parser.litkal └── sugar.litkal └── tests ├── assignment_statement.kal ├── classes.kal ├── comprehensions.kal ├── delete.kal ├── else_statement.kal ├── export.kal ├── expressions.kal ├── for_in_to_statement.kal ├── for_statement.kal ├── functions.kal ├── if_statement.kal ├── literate.litkal ├── other.kal ├── parallel_block.kal ├── regex.kal ├── return_statement.kal ├── strings.kal ├── timers.kal ├── trycatch.kal ├── unpack.kal ├── waitfor_statement.kal ├── when_expression.kal └── while_statement.kal /.gitignore: -------------------------------------------------------------------------------- 1 | # This project 2 | compiled 3 | 4 | # npm 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | ehthumbs.db 4 | $RECYCLE.BIN 5 | 6 | .git* 7 | *.kal 8 | dev 9 | source 10 | tests 11 | examples 12 | CHANGELOG.md 13 | .travis* 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: 3 | - npm install -g kal 4 | - mkdir compiled 5 | - npm run-script bootstrap 6 | node_js: 7 | - 0.6 8 | - 0.8 9 | - 0.10 10 | 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Change Log 2 | 3 | ### [0.5.6](https://github.com/rzimmerman/kal/compare/r0.5.6...r0.5.5) 4 | * Added support for block and multiline strings 5 | * Add the `from ... to .. as` feature 6 | * Added the `delete` statement 7 | * Fixes to implicit parentheses 8 | * Conditionals in comprehensions are supported 9 | * Added support for generators to `for` loops (like Python) 10 | * Added object unpacking support 11 | 12 | ### [0.5.5](https://github.com/rzimmerman/kal/compare/r0.5.5...r0.5.4) 13 | * For loops support the `at` index syntax (`for x at index in y`) 14 | * `is not` now works like `isnt` rather than `is (not` 15 | * Fixed command-line execution of scripts 16 | * Late bound methods no longer cause the class to be defined in scope (bug) 17 | 18 | ### [0.5.4](https://github.com/rzimmerman/kal/compare/r0.5.4...r0.5.3) 19 | * Fixed support for using the `arguments` structure in list comprehensions. 20 | * Fixed a bug where Kal would allow compilation of class definitions with bad class names 21 | * Converted source to Literate Kal. 22 | 23 | ### [0.5.3](https://github.com/rzimmerman/kal/compare/r0.5.2...r0.5.3) 24 | * Bug fixes to Literate Kal support 25 | 26 | ### [0.5.2](https://github.com/rzimmerman/kal/compare/r0.5.1...r0.5.2) 27 | * Added Literate Kal support 28 | 29 | ### [0.5.1](https://github.com/rzimmerman/kal/compare/r0.5.0...r0.5.1) 30 | * Fixed bugs in `run in parallel` blocks 31 | * `none` and `nothing` keywords now working as aliases for `null` 32 | * Added `bitwise left` (`<<`) and `bitwise right` (`>>`) shift operators. 33 | * Added support for late binding of methods to classes. 34 | * Fixed issues with Unicode support for international characters 35 | 36 | ### [0.5.0](https://github.com/rzimmerman/kal/compare/r0.4.9...r0.5.0) 37 | * Some cleanup to internal variable names and spacing in generated JavaScript. 38 | * Compile-time checks for JavaScript/Kal keywords being used as identifiers. 39 | * Async callbacks after `wait for`s are now consistent about running on the next process tick. The initial call runs synchronously. 40 | 41 | ### [0.4.9](https://github.com/rzimmerman/kal/compare/r0.4.8...r0.4.9) 42 | * `this`/`me` is now preserved between `wait for` calls 43 | * Added `until` loops (equivalent to `while not`) 44 | * Using anything other than a literal for an object key is now a compile time error (it used to just produce bad JavaScript). 45 | * The command line utility now accepts hyphenated file names (bug fix). 46 | * Added the `run in parallel` block feature for kicking off parallel tasks. 47 | * You can now define a function in an object definition. 48 | * CoffeeScript-style functions can now be the first argument to a function call without extra parentheses. 49 | * Fixed issues with the `^` operator. 50 | 51 | ### [0.4.8](https://github.com/rzimmerman/kal/compare/r0.4.7...r0.4.8) 52 | * Added support for default arguments in functions, methods, and tasks. 53 | * A compound assignment (like `+=`) no longer causes a variable to be declared. `a += 1` with no other definition of a will now throw an error, not return `NaN`. 54 | * Fixed bugs with `pause for` statements 55 | * Tasks now work as class members. 56 | 57 | ### [0.4.7](https://github.com/rzimmerman/kal/compare/r0.4.6...r0.4.7) 58 | * Added the `mod` (modulo) keyword 59 | * Removed all dependencies including uglify-js. `kal --minify` replaces `kal -f minify` and requires that you already have uglify installed. 60 | * Comments that start with `##` no longer get cut off 61 | * Output is beautified by default. The `concise` option no longer exists. 62 | * Added the `pause for` syntax 63 | * Bug fixes for consecutive `try` blocks with `wait for`s. 64 | * `wait for`s no longer need to have return values (e.g. `wait for task()` is now valid) 65 | * Fixed multi-line mode (Ctrl-V) in the REPL 66 | 67 | ### [0.4.6](https://github.com/rzimmerman/kal/compare/r0.4.5...r0.4.6) 68 | * Fixes to the `but` operator. It was failing to compile in certain cases, such as when combined with `not`. 69 | * Added support for bitwise operators (see the README for a full list). 70 | * Fixed an issue where you could not have an `if` statement directly after a multiline object definition. 71 | * Multiple return values from `wait for`s now actually work. 72 | * Fixed a (cosmetic) issue with JavaScript output for standard `return` statements. 73 | 74 | ### [0.4.5](https://github.com/rzimmerman/kal/compare/r0.4.4...r0.4.5) 75 | * Cleaned up the npm configuration for users building the repo manually (does not affect npm repository packages) 76 | * Fixed a bug with the command line tool where things like list comprehensions were not working when running a script directly with `kal` 77 | 78 | ### [0.4.4](https://github.com/rzimmerman/kal/compare/r0.4.3...r0.4.4) 79 | * Fixed a bug with `try` blocks containing `wait for` statements that could throw unexpected errors when nested in certain ways 80 | * Improvements to the command-line tool 81 | * Added the ability to compile a whole directory and recursive directories to the command-line tool 82 | * Fixed some issues with the `in` and `instanceof` operators in the REPL 83 | 84 | ### [0.4.3](https://github.com/rzimmerman/kal/compare/r0.4.2...r0.4.3) 85 | * Fixed a bug with `for parallel` blocks that could cause them to return multiple times 86 | * Better syntax error reporting in `catch` blocks 87 | * Added support for `catch` blocks with no error variable 88 | * `wait for` statements now work in the main file body instead of just in functions 89 | * Added support for `safe wait for` for functions that don't call back with an error argument 90 | * `wait for` statements now work in the REPL and bare files 91 | * Added the `pass` keyword for blank statements (like Python) 92 | * Added the `print` keyword as an alias for `console.log` 93 | 94 | ### [0.4.2](https://github.com/rzimmerman/kal/compare/r0.4.1...r0.4.2) 95 | * Added support for no-brace multiline CoffeeScript style object assignments 96 | * Fixed a bug with double quoted strings as the first argument to function calls 97 | * Better error messages for when `wait for` statements get included in `catch` blocks 98 | 99 | ### [0.4.1](https://github.com/rzimmerman/kal/compare/r0.4.0...r0.4.1) 100 | * Fixed the REPL and executable to actually work correctly 101 | 102 | ### [0.4.0](https://github.com/rzimmerman/kal/compare/r0.3.2...r0.4.0) 103 | * Added support for `wait for` statements which simplify callback use 104 | * `wait for` statements are supported inside conditionals and loops 105 | * `for` loops have a `parallel` or `series` specifier available for when they contain `wait for` statements 106 | * `try` blocks also support `wait for` statements 107 | * Fixed some bugs with tail conditionals 108 | * Fixed a bug where parentheses were not optional if the last line of a file was a function call 109 | * Output files are now beautified by default using uglify-js. They can also be minified or "compact" which is just standard output without most of the whitespace. 110 | * Known issues: the REPL is not really complete (scope is reset between each line) and running a script directly from the command line using `kal script.kal` doesn't always work right. Compiling a script using `kal -o out_dir/ script.kal` works fine. 111 | 112 | ### [0.3.2](https://github.com/rzimmerman/kal/compare/r0.3.1...r0.3.2) 113 | * Better error reporting from the compiler 114 | * List comprehension over objects is supported using the syntaxes: 115 | 116 | [expr(p) for property p of y] 117 | [expr(v) for property value v of y] 118 | [expr(p,v) for property p with value v of y] 119 | 120 | * Fixed a bug where you couldn't call a function with a list argument and implicit parens like `myfunc [1,2,3]` 121 | * Multi-line lists with optional commas (like CoffeeScript) are now supported. Multiline objects are not supported yet. 122 | * Some fixes to the command line tool to get it to work with node.js 0.6.x. No promises, though. 123 | 124 | ### [0.3.1](https://github.com/rzimmerman/kal/compare/r0.3.0...r0.3.1) 125 | * Support for list comprehensions using the `[expr(x) for x in y]` syntax 126 | 127 | ### [0.3.0](https://github.com/rzimmerman/kal/compare/r0.2.9...r0.3.0) 128 | * Indentation on blank lines is now ignored 129 | * The `Kal.eval` options argument is now optional 130 | * Better Javascript output for comments 131 | 132 | ### [0.2.9](https://github.com/rzimmerman/kal/compare/r0.2.8...r0.2.9) 133 | * Significant performance improvements for compile time 134 | * Fixed the command line options to the `kal` executable 135 | * Much better error reporting for syntax errors 136 | * Fixed a bug to allow no-parentheses function calls in for and while loop headers 137 | 138 | ### [0.2.8](https://github.com/rzimmerman/kal/compare/r0.2.7...r0.2.8) 139 | * Fixed some issues with escape sequences in strings 140 | * Fixed some issues with conditional return statements 141 | * Included an interactive shell which now runs be default if you run `kal` with no arguments. 142 | 143 | ### [0.2.7](https://github.com/rzimmerman/kal/compare/r0.2.6...r0.2.7) 144 | * Added a proper npmignore file to reduce the package size 145 | * Added this change log 146 | * Added support for reserved words as properties and function calls to reserved word properties 147 | 148 | ### [0.2.6](https://github.com/rzimmerman/kal/compare/r0.2.5...r0.2.6) 149 | * Now correctly parses and compiles expressions inside of double-quoted strings. Previous versions just 150 | treated these as Javascript 151 | * Added support for the `super` keyword 152 | 153 | ### [0.2.5](https://github.com/rzimmerman/kal/tree/8d994cca210638b2ac2518a2f7bbe598e067a418) - Nov 25 2012 154 | * Fixes reported version (was reporting 0.2.3 for version 0.2.4) 155 | 156 | ### [0.2.4](https://github.com/rzimmerman/kal/tree/c6a34fc132f15a10b787e5814d89648e27061aee) - Nov 25 2012 157 | * Added a `kal` command line tool 158 | * Still reports version 0.2.3 in some places (bug) 159 | 160 | ### [0.2.3](https://github.com/rzimmerman/kal/tree/f5a8cac5bace0a3d96b92f4d125a09026a4b9ae2) - Nov 25 2012 161 | * Fixes to trailing conditionals on function calls with implicit parentheses 162 | 163 | ### [0.2.2](https://github.com/rzimmerman/kal/tree/4798522fef3e41fc40f2b7819cd41c75a1b1f16a) - Nov 25 2012 164 | * Fixed the npm package.json file (was causing failed installs) 165 | 166 | ### [0.2.1](https://github.com/rzimmerman/kal/tree/914db52fa3a158c36b22bbde3480d9d8ba5bec3f) - Nov 25 2012 167 | * Added support for negative unary expressions (`-3`) 168 | * Added support for `!=` and `==` 169 | * Full support for function calls without parentheses 170 | * Added support for `not in` and `not of` operators 171 | * Removed CoffeeScript source files 172 | 173 | ### [0.2.0](https://github.com/rzimmerman/kal/tree/63075434b0343520d0c4f9c7a0460742108d96b9) - Nov 24 2012 174 | * Removed dependency on CoffeeScript, thought the .coffee files are still included for reference 175 | 176 | ### [0.1.0](https://github.com/rzimmerman/kal/tree/021497d75468bd648bf36944d5ab528f7185b8c9) - Nov 24 2012 177 | * Initial release 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Rob Zimmerman 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kal 2 | 3 | Kal is a highly readable, easy-to-use language that compiles to JavaScript. It's designed to be asynchronous and can run both on [node.js](http://nodejs.org/) and in the browser. Kal makes asynchronous programming easy and clean by allowing functions to [pause and wait for I/O](#asynchronous-wait-for), replacing an awkward callback syntax with a clean, simple syntax. 4 | 5 | For an overview, see [the Github page](http://rzimmerman.github.io/kal/) or check out the [examples](https://github.com/rzimmerman/kal/tree/master/examples). 6 | 7 | Kal is _expressive_ and offers many useful synonyms and constructs to make code readable in almost plain English. 8 | 9 | Kal is designed with a unique philosophy: 10 | 11 | 1. Eliminate the yucky parts of JavaScript, but keep the good stuff including the compatibility, and the great server and client runtime support. 12 | 2. Make code as readable as possible and make writing code straightforward. Eliminate the urge (and the need) to be terse and complicated. 13 | 3. Provide an alternative to callbacks (which look weird) and promises (which are weird) while providing excellent, easy-to-use asynchronous support. 14 | 15 | Check out the [examples](./examples) for some sample use cases. 16 | 17 | ## Installation Using npm 18 | 19 | This is the preferred method for installing Kal. Make sure you have installed [node.js](http://nodejs.org/). Kal works with versions 0.6, 0.8, and 0.10. It might work with other versions as well. Install the latest "stable" release of Kal using npm: 20 | 21 | ``` 22 | sudo npm install -g kal 23 | ``` 24 | 25 | `sudo` may not be required depending on how you installed `node`. 26 | 27 | ## Syntax Highlighting 28 | 29 | A [TextMate bundle](https://github.com/rzimmerman/kal.tmbundle) for TextMate and Sublime Text is available with limited but very useful support for Kal's syntax. 30 | 31 | [Vim support](https://github.com/bcho/kal-vim) is also available thanks to @bcho. 32 | 33 | ## Help and Support 34 | 35 | Visit the [Google group](https://groups.google.com/forum/#!forum/kal-programming-language) to ask questions and interact. 36 | 37 | File an issue on Github or send a pull request if you have something to add! 38 | 39 | ## Installing from the Repository 40 | 41 | [![Build Status](https://secure.travis-ci.org/rzimmerman/kal.png?branch=master)](https://travis-ci.org/rzimmerman/kal) 42 | 43 | If you need the latest and greatest (possibly unstable/broken) build, you can build Kal manually. Most users can skip this section and just use the latest `npm` version. 44 | 45 | Kal is written in Kal, so you need a prebuilt version of the compiler available to do the initial build: 46 | 47 | ``` 48 | sudo npm install -g kal 49 | ``` 50 | 51 | Then you can clone the repo, install the developer dependencies, and build the compiler: 52 | 53 | ``` 54 | git clone https://github.com/rzimmerman/kal kal 55 | cd kal 56 | npm install 57 | npm run-script make 58 | ``` 59 | 60 | Run the tests to make sure everything is going well: 61 | 62 | ``` 63 | npm test 64 | ``` 65 | 66 | If you're extra serious, you can use your new build to rebuild itself in case there were any notable changes to the compiler between the npm release and the latest commit. This will also run the tests. 67 | 68 | ``` 69 | npm run-script bootstrap 70 | ``` 71 | 72 | Now install your latest version using npm: 73 | 74 | ``` 75 | npm pack 76 | ``` 77 | 78 | Assuming the tests pass, this will make an archive file that you can install (the filename depends on the version): 79 | 80 | ``` 81 | sudo npm install -g kal-0.x.x.tgz 82 | ``` 83 | 84 | Alternatively you can just run the `scripts/kal` file if you don't want to install it globally. 85 | 86 | ## Usage 87 | 88 | If you installed Kal globally (using the `-g` option), you can run the interactive shell by running `kal` with no arguments. 89 | 90 | ``` 91 | $ kal 92 | kal> 'hello' + ' ' + 'world' 93 | 'hello world' 94 | ``` 95 | 96 | You can use the kal utility to run or compile files. Run `kal -h` for the full option set. If you installed kal locally (didn't use the -g option), you will need to specify the path to the kal executable, usually located at `node_modules/kal/scripts/kal`. 97 | 98 | ``` 99 | kal path/to/file.kal --runs the specified file 100 | kal -o path/for/output path/to/file1.kal path/to/file2.kal ... --compiles all files/directories listed to javascript 101 | and writes the output into the folder specified by -o 102 | ``` 103 | 104 | Using the `-j` or `--javascript` switches will show the output of the compiler. 105 | 106 | If you import Kal in your Javascript code, it installs a compile hook that allows you to directly import .kal files: 107 | 108 | ```javascript 109 | require('kal'); 110 | require('./mykalfile'); //refers to mykalfile.kal 111 | ``` 112 | 113 | ## Literate Kal 114 | 115 | Literate Kal offer an exciting way to write well-documented, readable code. The idea is based on [Literate CoffeeScript](http://coffeescript.org/#literate). The compiler will treat any file with a `.litkal` or `.md` extension as a [Markdown](http://daringfireball.net/projects/markdown/) document. Code blocks, denoted by four spaces of indentation, are treated as Kal code while anything that is not indented is treated as a comment. See [this example](https://github.com/rzimmerman/kal/blob/master/examples/literate.litkal) of a Literate Kal file. New in 0.5.2. 116 | 117 | As of 0.5.4, Kal is written in Literate Kal. 118 | 119 | ## Whitespace and Indentation 120 | 121 | In Kal, spaces for indentation are significant and tabs are not valid. Indents are required for function definitions and blocks of code inside of `if` statements, `try`/`catch` blocks, and loops. 122 | 123 | You should use two spaces to denote an indent. You can technically use any multiple of two spaces, but two is recommended as a style guideline. Any whitespace on blank lines is ignored. Semicolons at the end of statements are not required nor are they valid. 124 | 125 | In general single statements cannot contain line breaks. Notable exceptions are list and object definitions. For example: 126 | 127 | ```kal 128 | a = [1, 2, 129 | 3, 130 | 4] 131 | 132 | b = {a:1 133 | c:2} 134 | ``` 135 | 136 | Will work, however: 137 | 138 | ```kal 139 | a = 1 + 140 | 1 141 | ``` 142 | 143 | Is invalid. Future versions may include better support for line breaks within statements. 144 | 145 | ## Comments 146 | 147 | Comments are preceeded by a `#` sign. Anything after the `#` on the line will be ignored. 148 | 149 | ```kal 150 | print 5 #this is a comment 151 | ``` 152 | 153 | Multiline comments are enclosed by `###`: 154 | 155 | ```kal 156 | ### 157 | A multiline 158 | comment 159 | ### 160 | ``` 161 | 162 | ## Functions and Tasks 163 | 164 | **Functions** are defined with an optional name and a list of arguments. 165 | 166 | ```kal 167 | function my_function(arg1, arg2) 168 | return arg1 + arg2 169 | ``` 170 | 171 | and 172 | 173 | ```kal 174 | my_function = function (arg1, arg2) 175 | return arg1 + arg2 176 | ``` 177 | 178 | Both define a variable `my_function` that takes two arguments and returns their sum. CoffeeScript syntax is also valid: 179 | 180 | ```kal 181 | my_function = (arg1, arg2) -> 182 | return arg1 + arg2 183 | ``` 184 | 185 | But is generally discouraged unless it significantly helps readability. It was originally included to ease porting of the Kal compiler from CoffeeScript to Kal. Coffee-style functions must contain a line break after the `->`. `=>` is not supported. 186 | 187 | Functions can have default arguments. These will be used if the specified argument is `null` or `undefined`: 188 | ```kal 189 | function default_args(x,y=2) 190 | return x + y 191 | 192 | print default_args(1) # prints 3 193 | ``` 194 | 195 | Functions are called using parentheses. 196 | 197 | ```kal 198 | my_function(1, 2) 199 | ``` 200 | 201 | Will return `3`. Parentheses are optional if the function has at least one argument: 202 | 203 | ```kal 204 | my_function 1, 2 205 | ``` 206 | 207 | Is also valid. Function calls can be chained this way as well, so any of the following 208 | 209 | ```kal 210 | print(my_function(1,2)) 211 | print my_function 1, 2 212 | print my_function(1, 2) 213 | ``` 214 | 215 | will all print `3`. When calling a function with no arguments, parentheses are required. 216 | 217 | **Tasks** are similar to functions, except that they are intended to be called asynchronously (usually using a `wait for` statement). 218 | 219 | ```kal 220 | task my_task(arg) 221 | return arg * 2 222 | ``` 223 | 224 | or 225 | 226 | ```kal 227 | my_task = task (arg) 228 | return arg * 2 229 | ``` 230 | 231 | Tasks should not be called synchronously. If a task is called synchronously, it will return with no value. When called asynchronously. 232 | 233 | ```kal 234 | print my_task 1 235 | ``` 236 | 237 | Is valid syntax, but will print `undefined`. 238 | 239 | ```kal 240 | wait for x from my_task(1) 241 | print x 242 | ``` 243 | 244 | Will print `2` as expected. See the `wait for` section for more details on asynchronous calls. 245 | 246 | ## Objects and Arrays 247 | 248 | Objects and arrays are defined similarly to JavaScript. Newlines **are** valid inside of an array or object definition and indentation is ignored. Commas are optional when followed by a newline. CoffeeScript-style object definitions (no `{}`s) are only valid in assignments and must be preceded by a newline. 249 | 250 | ```kal 251 | a = [1, 2, 3] 252 | b = [1 253 | 2, 254 | 3 255 | 4] 256 | c = {a:1,b:2,c:{d:3}} 257 | d = 258 | a:1, b:2 259 | c: 260 | d:3 261 | ``` 262 | 263 | Function definitions are only valid in CoffeeScript-style object definitions at this time. 264 | 265 | ```kal 266 | d = 267 | a:1, b:2 268 | c: 269 | d: function () 270 | return 2 271 | e: -> 272 | return 3 273 | ``` 274 | 275 | Objects work like JavaScript objects (because they are JavaScript objects), so you can access members either using array subscripts or `.` notation 276 | 277 | ```kal 278 | x = 279 | a : 1 280 | b : 2 281 | print x['a'] #prints 1 282 | print x.b #prints 2 283 | ``` 284 | 285 | ## Scoping 286 | 287 | Variables are declared automatically and scoped within the current function unless used globally (like CoffeeScript). 288 | 289 | By default, nothing in your `.kal` file will leak to the global scope. Everything is wrapped within a function scope inside the module. If you need to export variables to global scope, you should use 290 | 291 | ```kal 292 | module.exports.my_export = my_variable_or_function # in node.js 293 | window.my_export = my_variable_or_function # in a browser 294 | ``` 295 | 296 | _or_ you can compile the file with the `--bare` option. 297 | 298 | ## Conditionals 299 | 300 | The following defines a conditional statement: 301 | 302 | ```kal 303 | x = 5 304 | if x is 5 305 | print 'five' 306 | ``` 307 | 308 | will print `five`. 309 | 310 | ```kal 311 | x = 6 312 | if x is 5 313 | print 'five' 314 | else 315 | print 'not five' 316 | ``` 317 | 318 | will print `not five` 319 | 320 | The conditional has useful synonyms: 321 | 322 | * `when` is equivalent to `if` 323 | * `unless` is equivalent to `if not` 324 | * `except when` is equivalent to `if not` 325 | * `otherwise` is equivalent to `else` 326 | 327 | So the following is valid, as are other permutations: 328 | 329 | ```kal 330 | unless name is 'Steve' 331 | print 'Impostor!' 332 | otherwise 333 | print 'Steve' 334 | ``` 335 | 336 | `else` (and synonyms) can be chained with `if` (and synonyms), so 337 | 338 | ```kal 339 | if name is 'Steve' 340 | print 'Steve' 341 | else if name is 'Brian' 342 | print 'Brian' 343 | otherwise when name is 'Joe' 344 | print 'Joe' 345 | else 346 | print 'Somebody' 347 | ``` 348 | 349 | is valid. 350 | 351 | Conditionals can also tail a statement: 352 | 353 | ```kal 354 | print 5 if 5 > 10 355 | ``` 356 | 357 | will do nothing. 358 | 359 | Conditionals can be used in a ternary statement as well 360 | 361 | ```kal 362 | print(5 if name is 'Joe' otherwise 6) 363 | ``` 364 | 365 | will print `5` if the variable name is equal to `'Joe'`, otherwise it will print `6`. Kind of like it says. Parentheses are required because tail conditionals associate right, meaning the following are equivalent: 366 | 367 | ```kal 368 | print 5 if name is 'Joe' otherwise 6 369 | (print(5) if name is 'Joe') otherwise 6 370 | ``` 371 | 372 | ## Loops 373 | 374 | `for` loops work as follows: 375 | 376 | ```kal 377 | for x in [1,2,3] 378 | print x 379 | ``` 380 | 381 | Will print the numbers 1, 2, and 3. The value n the right of the `for ... in` expression is called the `iterant`. Currently it must be an array. Python-like iterable object support is coming soon. 382 | 383 | `for ... in` loops can also have an index variable: 384 | 385 | ```kal 386 | for x at index in [10,20,30] 387 | print index, x 388 | ``` 389 | 390 | Will print the `0 10`, `1 20`, `2 30`. 391 | 392 | `for` loops can also be used on objects: 393 | 394 | ```kal 395 | obj = {a:1,b:2} 396 | for key of obj 397 | print key, obj[key] 398 | ``` 399 | 400 | Will print `a 1` and `b 2`. 401 | 402 | When used on asynchronous code, the `parallel` and `series` specifiers are available: 403 | 404 | ```kal 405 | for parallel x in y 406 | wait for z from f(x) 407 | 408 | for series x in y 409 | wait for z from f(x) 410 | ``` 411 | 412 | `series` is the default if neither is specified. Parallel for loops are **not** guaranteed to execute in order! In fact, they often won't. Take special care when accessing variables separated by `wait for` asynchronous statements. Remember that a `wait for` releases control of execution, so other loop iterations running in parallel may alter local variables if you are not careful. See the `wait for` section for more details. 413 | 414 | You can use a generator object as follows: 415 | 416 | ```kal 417 | for val from generator_object 418 | print val 419 | ``` 420 | 421 | A generator object is any object that has a `next()` method. The loop will keep calling the `next()` method until it does not return a value (returns `undefined` in JavaScript-speak). 422 | 423 | You can also use a range of numbers as an implicit generator to save memory: 424 | 425 | ```kal 426 | for val from 1 to 100000000 # will not instantiate a giant list 427 | print val 428 | ``` 429 | 430 | `while` loops continuously run their code block until a condition is satisfied. 431 | 432 | ```kal 433 | x = 0 434 | while x < 5 435 | x += 1 436 | print x 437 | ``` 438 | 439 | prints the numbers 1 through 5. `until` provides a similar function. 440 | 441 | ```kal 442 | x = 0 443 | until x is 5 444 | x += 1 445 | print x 446 | ``` 447 | 448 | ## Comprehensions 449 | 450 | List comprehensions are a quick and useful way to create an array from another array: 451 | 452 | ```kal 453 | y = [1,2,3] 454 | x = [value * 2 for value in y] 455 | ``` 456 | 457 | will set `x` equal to `[2,4,6]`. Comprehensions also support an iterable object. Iterable objects support a `next()` method which returns the next value in the sequence each time it is called. When there are no more values in the sequence, it should return `null`. 458 | 459 | ```kal 460 | class RandomList: 461 | method initialize(size) 462 | me.counter = size 463 | method next() 464 | me.counter -= 1 465 | if me.counter >= 0 466 | return Math.random() 467 | else 468 | return null 469 | 470 | x = [r * 10 for r in new RandomList(20)] 471 | ``` 472 | 473 | will set `x` to an array of 20 random numbers between 0 and 10. 474 | 475 | List comprehensions work on objects, too: 476 | 477 | ```kal 478 | obj = {a:1, b:2} 479 | x = [p for property p in obj] # ['a','b'] 480 | x = [v for property value v in obj] # [1, 2] 481 | x = [p+v for propert p with value v in obj] # ['a1', 'b2'] 482 | ``` 483 | 484 | Conditionals in list comprehensions are also supported: 485 | 486 | ```kal 487 | new_list = [i for i in old_list if i > 5] 488 | ``` 489 | 490 | ## Operators And Constants 491 | 492 | Listed below are Kal's operators and their other-language equivalents. Note that Kal has a lot of synonyms for some keywords, all of which compile to the same function. 493 | 494 | | Kal | CoffeeScript | JavaScript | Function | 495 | |:--------------------------:|:-----------------------:|:-------------------------:|--------------------------------| 496 | | `true`, `yes`, `on` | `true`, `yes`, `on` | `true` | Boolean true | 497 | | `false`, `no`, `off` | `false`, `no`, `off` | `false` | Boolean false | 498 | | `and`, `but` | `and` | `&&` | Boolean and | 499 | | `or` | `or` | || | Boolean or | 500 | | `nor` | none | none | Boolean or, inverted | 501 | | `not` | `not`, `!` | `!` | Boolean not | 502 | | `xor`, `bitwise xor` | `^` | `^` | Bitwise xor | 503 | | `bitwise not` | `~` | `~` | Bitwise not (invert) | 504 | | `bitwise and` | `&` | `&` | Bitwise and | 505 | | `bitwise or` | | | | | Bitwise or | 506 | | `bitwise left` | `<<` | `<<` | Bitwise shift left | 507 | | `bitwise right` | `>>` | `>>` | Bitwise shift right | 508 | | `+`, `-`, `*`, `/`, `mod` | `+`, `-`, `*`, `/`, `%` | `+`, `-`, `*`, `/`, `%` | Math operators | 509 | | `^` | none | none | Exponent (`Math.pow`) | 510 | | `exists`, `?` | `?` | none | Existential check | 511 | | `doesnt exist` | none | none | Existential check (inverted) | 512 | | `is`, `==` | `is`, `==` | `===` | Boolean equality | 513 | | `isnt`, `is not`, `!=` | `isnt`, `==` | `!==` | Boolean inequality | 514 | | `>`, `>=`, `<`, `<=` | `>`, `>=`, `<`, `<=` | `>`, `>=`, `<`, `<=` | Boolean comparisons | 515 | | `me`, `this` | `@`, `this` | `this` | Current object | 516 | | `in`, `not in` | `in`, `not in` | none | Boolean search of array/string | 517 | | `of` | `of` | `in` | Boolean search of object | 518 | | `nothing`, `empty`, `null` | `null` | `null` | Null value | 519 | | `undefined` | `undefined` | `undefined` | no value | 520 | | `instanceof` | `instanceof` | `instanceof` | inheritance check | 521 | | `print` | `console.log` | `console.log` | alias for `console.log` | 522 | 523 | ## Exisential Checks 524 | 525 | Kal implements the same existential operator features of CoffeeScript, with the addition of the `exists` and `doesnt exist` keyword suffixes, which perform the same function as the `?` operator. Examples: 526 | 527 | ```kal 528 | a = {a:1} 529 | b = [1,2,3] 530 | 531 | print(c exists) # false 532 | print(c doesnt exist) #true 533 | print(c?) #false 534 | 535 | print(c.something) #throws an error! 536 | print(c?.something) #prints undefined 537 | 538 | print(a?.a) # 1 539 | print(a?.a?) # true 540 | print(b?[2]) # 3 541 | 542 | print c() # error! 543 | print c?() # prints undefined 544 | ``` 545 | 546 | ## Classes and Inheritence 547 | 548 | Classes are defined with member `method` definitions. Methods are just functions that are added to the prototype of new instance objects (in other words, they are available to all instances of a class). The `initialize` method, if present, is used as the constructor when the `new` keyword is used. `me` (or its synonym `this`) is used in methods to access the current instance of the class. `instanceof` checks if an object is an instance of a class. 549 | 550 | ```kal 551 | class Person 552 | method initialize(name) 553 | me.name = name 554 | method printName() 555 | print me.name 556 | method nameLength() 557 | return me.name.length 558 | 559 | jen = new Person('Jen') 560 | jen.printName() # prints 'Jen' 561 | print(jen instanceof Person) # prints true 562 | ``` 563 | 564 | Classes can inherit from other classes and override or add to their method definitions. The `super` keyword can be used in a method to call the same function in the parent class. 565 | 566 | ```kal 567 | class FrumpyPerson inherits from Person 568 | method printName() 569 | print 'Frumpy ' + me.name 570 | method nameLength() 571 | return 0 572 | 573 | sue = new FrumpyPerson('Sue') 574 | sue.printName() # prints 'Frumpy Sue' 575 | print(sue instanceof Person) # prints true 576 | print(sue instanceof FrumpyPerson) # prints true 577 | print(jen instanceof FrumpyPerson) # prints false 578 | ``` 579 | 580 | You can add or alter a method or task to a class after it is defined (or from another file) using late binding using the `of` keyword. 581 | 582 | ```kal 583 | class MyClass 584 | method my_method(v) 585 | me.v = v 586 | 587 | x = new MyClass() 588 | x.my_method(10) 589 | print x.v # prints 10 590 | 591 | method my_method(v) of MyClass 592 | me.v = v + 1 593 | 594 | method my_other_method() of MyClass 595 | print me.v 596 | 597 | x = new MyClass() 598 | x.my_method 10 599 | x.my_other_method() # prints 11 600 | ``` 601 | 602 | ## Try/Catch 603 | 604 | `try` and `catch` blocks work similarly to JavaScript/CoffeeScript. `finally` blocks are not supported yet but are coming eventually. The `throw` statement (and its synonyms `raise` and `fail with`) work like JavaScript as well. 605 | 606 | ```kal 607 | try 608 | a = 'horse' / 2 # what are we doing? this will throw an error! 609 | b = 1 # never runs 610 | catch e 611 | print 'caught it!', e 612 | ``` 613 | 614 | The `e` variable above stores the error object thrown with `throw` or by the system. You can give it any name you want and it is optional. The following is valid: 615 | 616 | ```kal 617 | try 618 | throw 'a string' 619 | b = 1 # never runs 620 | catch 621 | print 'caught it!' 622 | ``` 623 | 624 | `try`/`catch` blocks can be nested. `try` blocks can contain asynchronous `wait for` calls, but `catch` blocks cannot at this time. 625 | 626 | ## Strings 627 | 628 | Strings can either be double-quoted (`"`) or single quoted (`'`). Backslashes can be used to escape quotes within strings if necessary. 629 | 630 | ```kal 631 | x = 'this is a "string" with quotes in it' 632 | y = "so is 'this'" 633 | z = 'this one is, \'too\' but I\'m not proud of it' 634 | ``` 635 | 636 | ## String Interpolation 637 | 638 | Double-quoted strings can contain interpolated values using `#{...}` blocks. These blocks can contain any valid Kal expression (including variables and function calls). This is the recommended way to do string concatenation as it is usually more readable. 639 | 640 | ```kal 641 | print "This is a string with the number 3: #{1+1+1}" 642 | "This is a string with the number 3: 3" 643 | 644 | a = cow 645 | n = moo 646 | print "The #{a} says #{n}, #{n}, #{n}!" 647 | "The cow says, moo, moo, moo!" 648 | ``` 649 | 650 | ## Regular Expressions 651 | 652 | Kal supports JavaScript's regex syntax, but not CoffeeScript style block regex syntax. 653 | 654 | ## Asynchronous Wait For 655 | 656 | The `wait for` statement executes a `task` and pauses execution (yielding to the runtime) until the `task` is complete. The following reads a file asynchronously and prints its contents (in node.js). 657 | 658 | ```kal 659 | fs = require 'fs' 660 | wait for data from fs.readFile '/home/user/file.txt' 661 | print data.toString() 662 | ``` 663 | 664 | Note that: 665 | 666 | * For users familiar with node.js and JavaScript, `fs.readFile` is called with the file name argument and a callback. **You don't need to supply a callback.** 667 | * After the `wait for` line, execution is paused and other code can run. Keep this in mind if you have global variables that are modified asynchronously as they may change between the `wait for` line and the line after it. 668 | * Any errors reported by `fs.readFile` (returned via callback) **will be thrown automatically**. You should wrap the `wait for` in a `try`/`catch` if you want to catch these errors. 669 | 670 | `wait for` can be used to call your own asynchronous tasks. It can also be used within `for` and `while` loops, `try` blocks, `if` statements, and **any nesting combination** you can think of. Really! 671 | 672 | ```kal 673 | fs = require 'fs' 674 | task readFileSafe(filename) 675 | if 'secret' in filename 676 | throw 'Illegal Access!' 677 | else 678 | wait for d from fs.readFile filename 679 | return d 680 | 681 | for parallel filename in ['secret/data.txt', 'test.txt', 'test2.txt'] 682 | try 683 | wait for data from readFileSafe '/home/secret/file.txt' 684 | print data.toString() 685 | catch error 686 | print "ERROR: #{error}" 687 | print 'DONE!' 688 | ``` 689 | 690 | `wait for` can also be used without arguments by omitting the `from` keyword 691 | 692 | ```kal 693 | wait for my_task() 694 | ``` 695 | 696 | Some node.js API functions (like `http.get`) don't follow the normal convention of calling back with an error argument. For these functions you must use the `safe` prefix, otherwise it will throw an error: 697 | 698 | ```kal 699 | http = require 'http' 700 | safe wait for request from http.get 'http://www.google.com' 701 | print request.responseCode 702 | ``` 703 | 704 | `wait for` statements also support multiple return values 705 | 706 | ```kal 707 | wait for a, b from my_task() 708 | ``` 709 | 710 | ## Asynchronous Pause 711 | 712 | You can pause for a specified amount of time using the `pause for` keyword 713 | 714 | ```kal 715 | print 'starting' 716 | pause for 1 second 717 | print 'done!' 718 | ``` 719 | 720 | `pause for` uses JavaScript's `setTimeout` function. Note that the argument is in seconds, not milliseconds like `setTimeout`. 721 | 722 | The `second` keyword is optional. `seconds` also works. Use your best judgement to keep your code readable. 723 | 724 | ```kal 725 | pause for 2 # seconds is implied 726 | pause for 10 seconds # also valid 727 | milliseconds = 1293 728 | pause for milliseconds/1000 seconds # expressions are valid for the timeout 729 | ``` 730 | 731 | ## Parallel Tasks 732 | 733 | You can kick off tasks in parallel with the `run in parallel` block 734 | 735 | ```kal 736 | run in parallel 737 | task1() 738 | wait for task2 a, b, c 739 | wait for x from task3() 740 | safe wait for y, z from task4() 741 | print 'all tasks finished' 742 | ``` 743 | 744 | Code after the `run in parallel` block will not run until all tasks have completed. If any errors are thrown by one or more tasks, an array of errors will be thrown after all tasks in the block complete (or fail). Array elements are in the order that the tasks were specified. If no error was thrown by a task, its error element will be `undefined` (`doesnt exist` will be true). `safe` waits will not check for errors. 745 | 746 | ```kal 747 | try 748 | run in parallel 749 | task_that_fails() 750 | task_that_succeeds() 751 | catch errors 752 | print errors[0] # prints the error thrown by task_that_fails 753 | print errors[1] exists # prints false 754 | ``` 755 | 756 | ## Unpacking Objects 757 | 758 | A shorthand syntax for the following: 759 | 760 | ```kal 761 | a = myobj.a 762 | b = myobj.b 763 | c = myobj.sea 764 | d = myobj.subprop.d 765 | ``` 766 | 767 | is available with the `unpack into` statement: 768 | 769 | ```kal 770 | unpack myobj into a, b, sea as c, subprop.d as d 771 | ``` 772 | -------------------------------------------------------------------------------- /examples/async-files-ls.kal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kal 2 | 3 | # This demo prints out the name and size of each 4 | # file in the user's home directory. It retrieves 5 | # the file listing and size asynchronously using 6 | # a standard for loop. 7 | 8 | fs = require 'fs' 9 | path = require 'path' 10 | 11 | # `print` is just an alias for `console.log` 12 | print 'Reading files...' 13 | 14 | # asynchronous call to readdir 15 | wait for file_list from fs.readdir process.env.HOME 16 | 17 | # execution resumes when the API function calls back with a result 18 | for file_name in file_list 19 | 20 | # an async call within a loop works like you'd expect 21 | wait for stats from fs.stat path.join(process.env.HOME, file_name) 22 | print file_name, stats.size 23 | print 'Done!' 24 | -------------------------------------------------------------------------------- /examples/async-http-client.kal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kal 2 | 3 | # This demo executes GET requests in parallel and in series 4 | # using `for` loops and `wait for` statements. 5 | 6 | # Notice how the serial GET requests always return in order 7 | # and take longer in total. Parallel requests come back in 8 | # order of receipt. 9 | 10 | http = require 'http' 11 | 12 | urls = ['http://www.google.com' 13 | 'http://www.apple.com' 14 | 'http://www.microsoft.com' 15 | 'http://www.nodejs.org' 16 | 'http://www.yahoo.com'] 17 | 18 | # This function does a GET request for each URL in series 19 | # It will wait for a response from each request before moving on 20 | # to the next request. Notice the output will be in the same order as the 21 | # urls variable every time regardless of response time. 22 | # It is a task rather than a function because it is called asynchronously 23 | # This allows us to use `return` to implicitly call back 24 | task series_demo() 25 | # The `series` keyword is optional here (for loops are serial by default) 26 | total_time = 0 27 | 28 | for series url in urls 29 | timer = new Date 30 | 31 | # we use the `safe` keyword because get is a "nonstandard" task 32 | # that does not call back with an error argument 33 | safe wait for response from http.get url 34 | 35 | delay = new Date() - timer 36 | total_time += delay 37 | 38 | print "GET #{url} - #{response.statusCode} - #{response.connection.bytesRead} bytes - #{delay} ms" 39 | 40 | # because we are in a task rather than a function, this actually exectutes a callback 41 | return total_time 42 | 43 | # This function does a GET request for each URL in parallel 44 | # It will NOT wait for a response from each request before moving on 45 | # to the next request. Notice the output will be determined by the order in which 46 | # the requests complete! 47 | task parallel_demo() 48 | total_time = 0 49 | 50 | # The `parallel` keyword is only meaningful here because the loop contains 51 | # a `wait for` statement (meaning callbacks are used) 52 | timer = new Date 53 | for parallel url in urls 54 | 55 | # we use the `safe` keyword because get is a "nonstandard" task 56 | # that does not call back with an error argument 57 | safe wait for response from http.get url 58 | 59 | delay = new Date() - timer 60 | 61 | print "GET #{url} - #{response.statusCode} - #{response.connection.bytesRead} bytes - #{delay}ms" 62 | 63 | total_time = new Date() - timer 64 | # because we are in a task rather than a function, this actually exectutes a callback 65 | return total_time 66 | 67 | print 'Series Requests...' 68 | wait for time1 from series_demo() 69 | print "Total duration #{time1}ms" 70 | 71 | print '' 72 | 73 | print 'Parallel Requests...' 74 | wait for time2 from parallel_demo() 75 | print "Total duration #{time2}ms" 76 | process.exit() 77 | -------------------------------------------------------------------------------- /examples/async-http-server.kal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kal 2 | 3 | # This demo starts a web server on port 8888 on 4 | # the local machine, accessible in the browser from 5 | # http://localhost:8000/ 6 | 7 | # The root path '/' will give a listing of all files 8 | # in the current directory. Things to try: 9 | # * Click on the links. Files with .txt or .md extensions 10 | # will return their contents. Other files will return the 11 | # size of the file 12 | # * Try a bad path to see the 404 message 13 | # * Look at the terminal output 14 | 15 | 16 | # This script uses express.js, so make sure 17 | # you have run npm install to include dependencies 18 | 19 | fs = require 'fs' 20 | express = require 'express' 21 | 22 | app = express() 23 | 24 | # return a file list for the root path 25 | app.get '/', (req, res) -> 26 | print 'Request for directory listing' 27 | # this path returns HTML with links 28 | res.setHeader 'Content-Type', 'text/html' 29 | # asynchronous call to readdir to get the files list of the current directory 30 | wait for file_list from fs.readdir '.' 31 | # list comprehension to generate the list of links 32 | links = ["#{file_name}" for file_name in file_list] 33 | # string inlining to generate the HTML output 34 | res.send "Kal Demo#{links.join('
')}" 35 | 36 | app.get '/:file_name', (req, res) -> 37 | print "Request for #{req.params.file_name}" 38 | # this route always returns plain text 39 | res.setHeader 'Content-Type', 'text/plain' 40 | # get the file name from the request 41 | file_name = req.params.file_name 42 | # try to read the file, use the catch clause if it doesn't exist 43 | try 44 | # check if it's a text file or markdown file 45 | if file_name.match /.*\.(txt|kal)/ 46 | # for a text file, asynchronously read the contents 47 | wait for return_text from fs.readFile file_name 48 | print "...returning file contents for #{req.params.file_name}" 49 | else 50 | # for a non-text file, asynchronously read the file size 51 | wait for file_stats from fs.stat file_name 52 | return_text = 'File of size '+ file_stats.size 53 | print "...returning file size for #{req.params.file_name}" 54 | res.send return_text 55 | catch e 56 | print "...could not find #{req.params.file_name}" 57 | # if any function above calls back with an error, return a 404 58 | res.send 404, 'File not found' 59 | 60 | port = 8888 61 | app.listen 8888 62 | 63 | print "Listening on port #{port}" 64 | -------------------------------------------------------------------------------- /examples/conditionals.kal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kal 2 | 3 | # This demo shows some of the condition constructs 4 | # and synonyms used in Kal 5 | 6 | steve = {name: 'Steve', age: 25, clearance: 'Double Secret'} 7 | 8 | jen = {name: 'Jen', age: 27, clearance: 'None'} 9 | 10 | # if statements work like you'd expect (like CoffeeScript) 11 | if jen.age is 27 12 | print 'Jen is 27' 13 | 14 | if steve.name is 'Joe' 15 | print 'wat' 16 | else if steve.name is 'Sam' 17 | print 'que?' 18 | else 19 | print 'OK' 20 | 21 | # many keywords have synonyms to make code more English-like 22 | # `but` is equivalent to `and` 23 | when jen.age < 30 but jen.clearance isnt 'Double Secret' 24 | print 'ACCESS DENIED' 25 | otherwise 26 | print 'ACCESS GRANTED' 27 | 28 | unless steve.name is 'Steve' and jen.name is 'Jen' 29 | print 'bad data' 30 | else 31 | print 'OK' 32 | 33 | except when steve.name is 'Steve' 34 | print 'bad data' 35 | 36 | # Kal supports tail conditionals on statements 37 | name = jen.name if jen.age > 20 38 | age = steve.age except when steve.name is 'Steve' #assignment will not execute, age is undefined 39 | 40 | # Ternary operators also exist, and all synonyms work 41 | number = (20 if jen.name is 'Jen' otherwise 30) 42 | -------------------------------------------------------------------------------- /examples/lists-and-comprehensions.kal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kal 2 | 3 | # This demo shows some of the features of arrays, objects 4 | # and list/object comprehensions in Kal 5 | 6 | # simple list definition 7 | a = [1, 2, 3, 4, 5] 8 | 9 | # alternate syntax, commas are optional after line breaks 10 | a = [1, 2 11 | 3, 12 | 4 13 | 5] 14 | 15 | # simple comprehension 16 | b = [x * 2 for x in a] 17 | print b # [2, 4, 6, 8, 10] 18 | 19 | # boolean search 20 | print 1 in a # true 21 | print 1 in b # false 22 | 23 | # a simple for loop over a list 24 | for x in a 25 | print x 26 | 27 | # JavaScript operations all work as expected 28 | c = a.concat b 29 | c.push 100 30 | print c # [1, 2, 3, 4, 5, 2, 4, 6, 8, 10, 100] 31 | 32 | # JavaScript-like object definition 33 | g = {a:1,b:2,c:{n:1}} 34 | 35 | # Multi-line syntax 36 | h = 37 | a: 1 38 | b: 2 39 | c: 40 | n: 1 41 | 42 | # Loop iteration is similar to CoffeeScript 43 | # prints a 1, b 2, c {n:1} 44 | for key of g 45 | print key, g[key] 46 | 47 | 48 | # Comprehensions for objects 49 | keys = [k for property k in g] 50 | print keys # ['a', 'b', 'c'] 51 | vals = [v for property value v in g] 52 | print vals #[1, 2, {n:1}] 53 | kvs = [k + v.toString() for property k with value v in g] 54 | print kvs # ['a1','b2','c[object Object]'] 55 | 56 | -------------------------------------------------------------------------------- /examples/literate.litkal: -------------------------------------------------------------------------------- 1 | Literate Kal 2 | ============ 3 | 4 | This is an example of a **Literate Kal** file. It's the same as a markdown file, except embedded code blocks are actually compiled. 5 | 6 | print "This is actually compiled" 7 | lang = "Kal" 8 | print "Literate #{lang}" 9 | 10 | Code blocks can be seperated by documentation. 11 | 12 | print 'Still running...' 13 | 14 | Try it out! Remember that when compiled, any markdown content just turns into comments. So 15 | 16 | if 3 > 2 17 | 18 | Only one these will still run: 19 | 20 | print 'Condition true.' 21 | 22 | Even though they are separated into blocks 23 | 24 | else 25 | print 'Condition false!' 26 | 27 | Have fun. 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kal", 3 | "description": "Simple and useful", 4 | "keywords": ["javascript", "language", "kal", "coffeescript", "compiler"], 5 | "author": "Rob Zimmerman", 6 | "version": "0.5.6", 7 | "licenses": [{ 8 | "type": "MIT", 9 | "url": "https://raw.github.com/rzimmerman/kal/master/LICENSE" 10 | }], 11 | "engines": { 12 | "node": ">=0.6.0" 13 | }, 14 | "directories" : { 15 | "lib" : "./source/kal" 16 | }, 17 | "main" : "./compiled/kal", 18 | "bin": { 19 | "kal": "./scripts/kal" 20 | }, 21 | "scripts": { 22 | "make": "mkdir -p compiled && rm -f compiled/* && kal -o compiled/ source/*", 23 | "bootstrap": "npm run-script make && scripts/kal --minify -o compiled/ source/* && npm test", 24 | "test": "node_modules/mocha/bin/mocha -r should -R spec tests/* --compilers kal:compiled/kal" 25 | }, 26 | "homepage": "http://rzimmerman.github.io/kal", 27 | "bugs": "https://github.com/rzimmerman/kal/issues", 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/rzimmerman/kal.git" 31 | }, 32 | "dependencies": {}, 33 | "devDependencies": { 34 | "mocha": "1.6.x", 35 | "should": "1.2.x", 36 | "uglify-js": "2.3.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/kal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../compiled'); 6 | 7 | require(lib + '/command').run(); 8 | -------------------------------------------------------------------------------- /source/ast.litkal: -------------------------------------------------------------------------------- 1 | The Abstract Syntax Tree (AST) 2 | ------------------------------ 3 | 4 | This module defines error classes and the base abstract syntax tree class used by the parser and grammar. It's main purpose is to provide utility methods to parse the token array into classes that inherit from `ASTBase`. 5 | 6 | Errors 7 | ====== 8 | 9 | There are two types of errors that can be reported during parsing. `ParseFailed` is less serious indicating that the class failed to parse the upcoming tokens in the array, however the syntax might still be valid if parsed as another type of AST node. This generally will not abort compilation as the parent node will try other AST classes against the token stream before failing. `not_compiler_issue` indicates to the compiler that this is a problem with the input code, not a bug in the compiler itself. This error is used internally only. 10 | 11 | class ParseFailed 12 | method initialize(message) 13 | me.message = message 14 | me.not_compiler_issue = yes 15 | 16 | `SyntaxError` is more serious indicating that the parser is using the correct class to parse the stream, but the code has invalid syntax. For example, `if for x` would throw this error because the `IfStatement` class is sure that this is an `if` statement, but `if for` is definitely invalid. This always aborts compilation and gives the user an error. This error will get thrown to the top level module. 17 | 18 | class SyntaxError 19 | method initialize(message) 20 | me.message = message 21 | me.locked = yes 22 | me.not_compiler_issue = yes 23 | 24 | exports.SyntaxError = SyntaxError 25 | 26 | ASTBase Class 27 | ============= 28 | 29 | This class serves as a base class on top of which AST nodes are defined. It contains utility functions to parse the token stream. 30 | 31 | class ASTBase 32 | 33 | method initialize(ts, parent) 34 | 35 | The object is initially marked as "unlocked", indicating that we are not sure that this is the right node to parse this segment of the token stream. We can't declare syntax errors until we are sure this is the right class. 36 | 37 | me.locked = no 38 | 39 | Record the line and link to the token stream. Also note the parent object for code generation 40 | 41 | me.ts = ts 42 | me.line = ts.line 43 | me.ast_parent = parent 44 | 45 | The `parse` method, which is overriden by child classes, attempts to parse the current spot in the token stream. It will fail with a `ParseFailed` error or `SyntaxError` if parsing fails. 46 | 47 | me.parse() 48 | 49 | Record the last line of the node. This is used to place comments correctly in the output Javascript. 50 | 51 | me.endline = ts.line 52 | 53 | **opt** attempts to parse the token stream using one of the classes or token types specified. This method takes a variable number of arguments. For example, calling `me.opt IfStatement, Expression, 'IDENTIFIER'` would attempt to parse the token stream first as an `IfStatement`. If that fails, it would attempt to use the `Expression` class. If that fails, it will accept a token of type `IDENTIFIER`. If all of those fail, it will return `none`. 54 | 55 | Note that `SyntaxError`s will still be thrown, but not `ParseFailed` errors. 56 | 57 | method opt() 58 | 59 | If `opt` fails, it will reset the current index of the token stream. 60 | 61 | start_index = me.ts.index 62 | 63 | For each argument, which can be a class or a string, it will attempt to parse the token stream with that class or match the token type to the string. 64 | 65 | for cls in arguments 66 | 67 | For strings it just checks the token type. And returns the token (incrementing the token stream index) if the type matches. 68 | 69 | if typeof(cls) is 'string' 70 | if me.ts.type is cls 71 | token = me.ts.current 72 | me.ts.next() 73 | return token 74 | 75 | For classes, it attempts to instantiate the class (which calls the class's `parse`). `new` will fail with either a `ParseFailed`, `SyntaxError`, or other exception (if the compiler has a bug) if parsing fails. 76 | 77 | else 78 | try 79 | return new cls me.ts, me 80 | 81 | If parsing does fail, we want to rewind the token stream. If the error has the `locked` flag or the error came from the compiler, we do want to abort and throw. 82 | 83 | catch err 84 | me.ts.goto_token start_index 85 | fail with err when err.locked or not err.not_compiler_issue 86 | 87 | `opt` returns `nothing` if none of the arguments could be use to parse the stream. 88 | 89 | return nothing 90 | 91 | **req** works the same way as `opt` except that it throws an error if none of the arguments can be used to parse the stream. 92 | 93 | method req() 94 | 95 | We first call `opt` to see what we get. If a value is returned, the function was successful, so we just return the node that `opt` found. 96 | 97 | rv = me.opt.apply this, arguments 98 | return rv if rv exists 99 | 100 | If `opt` returned nothing, we want to give the user a useful error. 101 | 102 | list = [cls.name or cls for cls in Array.prototype.slice.call(arguments)] 103 | if list.length is 1 104 | message = "Expected #{list[0]}" 105 | else 106 | message = "Expected one of #{list.join(', ')}" 107 | me.error "#{message}" 108 | 109 | **opt_val** checks if the next token has a semantic value that matches one of the arguments provided. If so it returns that token and advances the stream. Otherwise, it returns `nothing`. 110 | 111 | method opt_val() 112 | if me.ts.value in arguments 113 | token = me.ts.current 114 | me.ts.next() 115 | return token 116 | else 117 | return nothing 118 | 119 | **req_val** is the same as `opt_val` except that it throws an error if the semantic value could not be matched. 120 | 121 | method req_val() 122 | rv = me.opt_val.apply this, arguments 123 | return rv if rv exists 124 | 125 | The JavaScript (node) `arguments` structure is a little weird in that it's not strictly an array. It's actually an object mapping with some extra properties. In order to produce a useful error message we make a copy of the values as an array. 126 | 127 | args = [v for v in Array.prototype.slice.call(arguments)] 128 | me.error "Expected '#{args.join('\' or \'')}'" 129 | 130 | **opt_multi** this method is like `opt` except that it will return zero or more of the requested class or token type. If it can't match any tokens it returns an empty array. It is "greedy" in that it will try to match as many occurances of a token or class as possible. `opt_multi` will only return objects/tokens from the first argument that matches the token stream. This method always returns an array. 131 | 132 | method opt_multi() 133 | cls = me.opt.apply this, arguments 134 | return [] unless cls exists 135 | rv = [cls] 136 | while cls exists 137 | cls = me.opt.apply this, arguments 138 | rv.push(cls) if cls exists 139 | return rv 140 | 141 | **req_multi** this method is like `req` except that it will return one or more of the requested class or token type. If it can't match any tokens it does throw an error. It is "greedy" in that it will try to match as many occurances as possible. `req_multi` will only return objects/tokens from the first argument that matches the token stream. This method always returns an array. 142 | 143 | method req_multi() 144 | rv = me.opt_multi.apply this, arguments 145 | return rv if rv.length > 0 146 | 147 | Create a useful error message for the user. 148 | 149 | list = [cls.name or cls for cls in Array.prototype.slice.call(arguments)] 150 | me.error "Expected one of #{list.join(', ')}" 151 | 152 | **parse** and **js** are abstract methods defined here to catch missing implementations. Child classes _must_ override these methods, otherwise compilation will fail. 153 | 154 | method parse() 155 | me.lock() 156 | me.error 'Parser Not Implemented: ' + me.constructor.name 157 | 158 | method js() 159 | me.error 'Javascript Generator Not Implemented: ' + me.constructor.name 160 | 161 | **error** throws a `ParseFailed` error if the object is unlocked (syntax does not match this class, but may still be valid for another node) or a `SyntaxError` if the object is locked (syntax matches this node but is invalid). 162 | 163 | method error(msg) 164 | if me.locked 165 | full_msg = msg + ' on line ' + me.line 166 | 167 | We try to report the filename if possible. 168 | 169 | if me.ts.options?['filename'] exists 170 | full_msg += ' in file ' + me.ts.options['filename'] 171 | throw new SyntaxError full_msg 172 | else 173 | throw new ParseFailed msg 174 | 175 | **lock** marks this class as locked, meaning we are certain this is the correct class for the given syntax. For example, if the `FunctionExpression` class sees the IDENTIFIER `function`, we are certain this is the correct class to use. Once locked, any invalid syntax causes compilation to fail. 176 | 177 | `lock` can be called multiple times to update the line number. If a node spans multiple lines, this is useful because the line number is reported in the error message. 178 | 179 | method lock() 180 | me.locked = yes 181 | me.line = me.ts.line 182 | 183 | ASTBase is the base class for all AST nodes. 184 | 185 | exports.ASTBase = ASTBase 186 | -------------------------------------------------------------------------------- /source/command.litkal: -------------------------------------------------------------------------------- 1 | Command Line Utility 2 | -------------------- 3 | 4 | This module defines the command line `kal` utility. 5 | 6 | fs = require 'fs' 7 | path = require 'path' 8 | Kal = require './kal' 9 | 10 | Utilities 11 | ========= 12 | 13 | Some messages are written to `stderr` in this module. 14 | 15 | function warn(line) 16 | process.stderr.write line + '\n' 17 | 18 | This utility function checks if a file is "hidden" by the operating system. 19 | 20 | function hidden(file) 21 | /^\.|~$/.test file 22 | 23 | `parseOptions` parses the command line switches. 24 | 25 | function parseOptions() 26 | options = {} 27 | for arg in process.argv 28 | if arg[0] is '-' and arg[1] isnt '-' 29 | options.help = yes if 'h' in arg 30 | options.tokens = yes if 't' in arg 31 | options.javascript = yes if 'j' in arg 32 | options.bare = yes if 'b' in arg 33 | options.version = yes if 'v' in arg 34 | options.minify = yes if 'm' in arg 35 | options.no_export = yes if 'e' in arg 36 | else if arg[0] is '-' and arg[1] is '-' 37 | options.help = yes if arg is '--help' 38 | options.tokens = yes if arg is '--tokens' 39 | options.javascript = yes if arg is '--javascript' 40 | options.bare = yes if arg is '--bare' 41 | options.version = yes if arg is '--version' 42 | options.minify = yes if arg is '--minify' 43 | options.no_export = yes if arg is '--no_export' 44 | 45 | The `-o` option has an argument (the output directory). 46 | 47 | if '-o' in process.argv 48 | index = process.argv.indexOf '-o' 49 | else if '--output' in process.argv 50 | index = process.argv.indexOf '--output' 51 | 52 | options.output = process.argv[index + 1] if index isnt -1 53 | 54 | The remaining arguments are assumed to be input file names. We loop through the array and ignore switches and the output directory (if any). 55 | 56 | inputs = [] 57 | for arg in process.argv.slice(2) 58 | if arg[0] is '-' or arg is options.output 59 | 60 | Set the help flag if the user passed input files (or extra arguments) that were followed by other switches like `kal -o output_dir some_file -j`. This is considered invalid. 61 | 62 | options.help = yes if inputs.length isnt 0 63 | inputs = [] 64 | Otherwise, add the argument to the list of input files. 65 | 66 | else 67 | inputs.push arg 68 | options._ = inputs 69 | return options 70 | 71 | `existsSync` is used to retain compatibility between node.js versions. 72 | 73 | existsSync = fs.existsSync or path.existsSync 74 | 75 | Main 76 | ==== 77 | 78 | function run() 79 | 80 | Parse the command line options and print the version/usage if necessary. 81 | 82 | options = parseOptions() 83 | return version() if options.version 84 | return usage() if options.help 85 | 86 | Check the output path (if specified) and make sure it is valid. 87 | 88 | if options.output exists and not existsSync(options.output) 89 | warn('output path does not exist!') 90 | return usage() 91 | 92 | If no input files are specified, start the interactive shell. 93 | 94 | return require('./interactive') if options._.length is 0 95 | 96 | Let scripts know we are running in `kal` not `node`. 97 | 98 | process.argv[0] = 'kal' 99 | process.execPath = require.main.filename 100 | 101 | Construct the `compile_options` argument for `Kal.compile` or `Kal.eval`. 102 | 103 | compile_options = 104 | show_tokens: options.tokens 105 | bare: options.bare 106 | show_js: options.javascript 107 | 108 | If an output argument was specified, we are writing JavaScript files to an output directory. 109 | 110 | if options.output exists 111 | 112 | Attempt to load `uglify-js` if the user wants to minify files. This is not listed as a dependency so the user needs it installed globally or manually. 113 | 114 | try 115 | require('uglify-js') if options.minify 116 | catch 117 | warn 'error: uglify-js must be installed to use the --minify option' 118 | process.exit(3) 119 | 120 | If the user just specified one directory, assume they just want all the files in it. Compile the list of files with the given options. 121 | 122 | if options._.length is 1 and fs.statSync(options._[0]).isDirectory() 123 | files = [path.join(options._[0],file) for file in fs.readdirSync(options._[0])] 124 | compile_files files, options.output, compile_options, options.minify 125 | else 126 | compile_files options._, options.output, compile_options, options.minify 127 | else 128 | 129 | If no output was specified, just run the script using `eval`. 130 | 131 | for filename in options._ 132 | compile_options.literate = path.extname(filename) in ['.litkal', '.md'] 133 | Kal.eval fs.readFileSync(filename), compile_options 134 | 135 | The `scripts/kal` loader calls this entry point. 136 | 137 | exports.run = run 138 | 139 | 140 | Compile Files 141 | ============= 142 | 143 | This function recursively compiles a list of files/directories into `output_dir`. 144 | 145 | function compile_files(filenames, output_dir, options, minify) 146 | for filename in filenames 147 | stat = fs.statSync filename 148 | 149 | If this file is a directory, get a list of files in the directory and call this function recursively. 150 | 151 | if stat.isDirectory() 152 | new_outdir = path.join(output_dir, path.basename(filename)) 153 | fs.mkdirSync new_outdir, stat.mode 154 | subfiles = [path.join(filename, child) for child in fs.readdirSync(filename)] 155 | compile_files subfiles, new_outdir, options, minify 156 | 157 | For `.kal`, `.litkal`, and `.md` (literate Kal assumed) files, set up the options structure and call `Kal.compile`. 158 | 159 | else if path.extname(filename) in ['.kal', '.litkal', '.md'] 160 | extension = path.extname(filename) 161 | 162 | Check if this is Literate code. 163 | 164 | options.literate = extension in ['.litkal', '.md'] 165 | 166 | Compile the source. 167 | 168 | js_output = Kal.compile fs.readFileSync(filename), options 169 | 170 | Minify if requested. We've already checked that `uglify-js` is installed at this point. 171 | 172 | if minify 173 | js_output = require('uglify-js').minify(js_output, {fromString:yes,mangle:no}).code 174 | 175 | Print out the JavaScript if the debug option was passed in. 176 | 177 | print js_output if options.show_js 178 | 179 | Write the output to the output directory with a `.js` extension. 180 | 181 | js_filename = path.join(output_dir, path.basename(filename, extension)) + '.js' 182 | fs.writeFileSync js_filename, js_output 183 | 184 | Version 185 | ======= 186 | 187 | Returns the Kal version when for the `-v` switch. 188 | 189 | function version() 190 | print "Kal version #{Kal.VERSION}" 191 | process.exit(0) 192 | 193 | Help options or invalid input will cause this message to print to the screen. 194 | 195 | function usage() 196 | print "Usage: kal [options] SOURCE [-o OUTPUT_DIR]" 197 | print "" 198 | print "If called without the -o option, `kal` will run SOURCE." 199 | print "If called without any options, `kal` will start an interactive session." 200 | print "" 201 | print "" 202 | print "Options:" 203 | print " --help, -h show the command line usage options [boolean]" 204 | print " --tokens, -t print out the tokens that the lexer/sugarer produce [boolean]" 205 | print " --javascript, -j print out the compiled javascript [boolean]" 206 | print " --bare, -b don't wrap the output in a function [boolean]" 207 | print " --version, -v display the version number [boolean]" 208 | print " --output, -o the output directory for the compiled source" 209 | print " --minify minify the output (requires uglify-js) [boolean]" 210 | print " --no_export, -e don't export top-level objects by default [boolean]" 211 | process.exit(2) 212 | -------------------------------------------------------------------------------- /source/interactive.litkal: -------------------------------------------------------------------------------- 1 | Kal Interactive Shell 2 | --------------------- 3 | 4 | The interactive shell is a read-evaluate-print loop (REPL) that compiles one line to Javascript and executes it, displaying the result to the user. 5 | 6 | Most of this was lovingly stolen from [CoffeeScript](http://coffeescript.org/documentation/docs/repl.html). 7 | 8 | The REPL starts by opening up `stdin` and `stdout`. 9 | 10 | stdin = process.openStdin() 11 | stdout = process.stdout 12 | 13 | The Kal compiler and the built-in node utilities are also used, including `util.inspect` for displaying pretty values of objects. Kal keywords are used to help autocomplete. 14 | 15 | Kal = require './kal' 16 | readline = require 'readline' 17 | util = require 'util' 18 | inspect = util.inspect 19 | vm = require 'vm' 20 | Script = vm.Script 21 | Module = require 'module' 22 | KAL_KEYWORDS = require('./grammar').KEYWORDS 23 | 24 | 25 | The prompt is five characters (with a space) and defaults to `kal> `. We tried to enable color output if the OS/shell supports it. We don't bother on Windows since it won't work with normal escape codes anyway. 26 | 27 | REPL_PROMPT = 'kal> ' 28 | REPL_PROMPT_MULTILINE = '---> ' 29 | REPL_PROMPT_CONTINUATION = '...> ' 30 | enableColors = no 31 | unless process.platform is 'win32' 32 | enableColors = not process.env.NODE_DISABLE_COLORS 33 | 34 | 35 | The error function will print the stack trace if it is available. 36 | 37 | function error(err) 38 | stdout.write err.stack or err.toString() 39 | stdout.write '\n' 40 | 41 | Autocompletion 42 | ============== 43 | 44 | These regexes match complete-able bits of text. 45 | 46 | ACCESSOR = /\s*([\w\.]+)(?:\.(\w*))$/ 47 | SIMPLEVAR = /(\w+)$/i 48 | 49 | The `autocomplete` function returns a list of completions, and the completed text. 50 | 51 | function autocomplete (text) 52 | return completeAttribute(text) or completeVariable(text) or [[], text] 53 | 54 | `completeAttribute` attempts to autocomplete a chained dotted attribute: `one.two.three`. 55 | 56 | function completeAttribute(text) 57 | match = text.match ACCESSOR 58 | if match 59 | all = match[0] 60 | obj = match[1] 61 | prefix = match[2] 62 | 63 | If the object doesn't exist (or running it causes an error), we abort autocomplete. 64 | 65 | try 66 | obj = Script.runInThisContext obj 67 | catch e 68 | return 69 | return when obj doesnt exist 70 | 71 | Otherwise we get property names and return them as a list, avoiding duplicates. 72 | 73 | obj = Object(obj) 74 | candidates = Object.getOwnPropertyNames obj 75 | obj = Object.getPrototypeOf obj 76 | while obj 77 | for key in Object.getOwnPropertyNames(obj) 78 | candidates.push key unless key in candidates 79 | obj = Object.getPrototypeOf obj 80 | completions = getCompletions prefix, candidates 81 | return [completions, prefix] 82 | 83 | `completeVariable` attempts to autocomplete an in-scope free variable like `one`. 84 | 85 | function completeVariable (text) 86 | free = text.match(SIMPLEVAR)?[1] 87 | free = "" if text is "" 88 | 89 | Get a list of variables by running `getOwnPropertyNames` on `this`. 90 | 91 | if free exists 92 | vars = Script.runInThisContext 'Object.getOwnPropertyNames(Object(this))' 93 | keywords = [] 94 | 95 | Include keywords as possible matches unless they start with `__`. 96 | 97 | for r in KAL_KEYWORDS 98 | keywords.push r when r.slice(0,2) isnt '__' 99 | candidates = vars 100 | for key in keywords 101 | candidates.push key when not (key in candidates) 102 | completions = getCompletions free, candidates 103 | return [completions, free] 104 | 105 | `getCompletions` returns elements of candidates for which `prefix` is a prefix. 106 | 107 | function getCompletions(prefix, candidates) 108 | rv = [] 109 | for el in candidates 110 | rv.push el when 0 is el.indexOf prefix 111 | return rv 112 | 113 | Exceptions 114 | ========== 115 | 116 | Make sure that uncaught exceptions don't kill the REPL. 117 | 118 | process.on('uncaughtException', error) 119 | 120 | Running the REPL 121 | ================ 122 | 123 | The current backlog of multi-line code. 124 | 125 | backlog = '' 126 | 127 | The current sandbox. We run in the current scope because certain globals (like Array) are not identical in a sandbox. For example, [1,2] instanceof Array would be false in a sandbox. 128 | 129 | sandbox = global 130 | 131 | The main REPL function, **run**, is called every time a line of code is entered. We attempt to evaluate the command. If there's an exception, we print it out instead of exiting. 132 | 133 | function run(buffer) 134 | 135 | Remove single-line comments 136 | 137 | buffer = buffer.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, "$1$2$3" 138 | 139 | Remove trailing newlines. 140 | 141 | buffer = buffer.replace /[\r\n]+$/, "" 142 | 143 | If we are in multiline mode, just add text to the backlog. 144 | 145 | if multilineMode 146 | backlog += "#{buffer}\n" 147 | repl.setPrompt REPL_PROMPT_CONTINUATION 148 | repl.prompt() 149 | return 150 | 151 | If there was nothing entered, don't bother to evaluate it - just print a new prompt. 152 | 153 | if buffer.toString().trim() is "" and backlog is "" 154 | repl.prompt() 155 | return 156 | 157 | Otherwise, update the backlog. 158 | 159 | backlog += buffer 160 | code = backlog 161 | 162 | Check for a line continuation character and give another prompt line if one was found. 163 | 164 | if code[code.length - 1] is '\\' 165 | backlog = "#{backlog.slice(0,-1)}\n" 166 | repl.setPrompt REPL_PROMPT_CONTINUATION 167 | repl.prompt() 168 | return 169 | 170 | If we made it this far, we are ready to execute `code`. Reset the prompt and backlog then make the sandbox. 171 | 172 | repl.setPrompt REPL_PROMPT 173 | backlog = "" 174 | 175 | We keep the same sandbox between runs, so only create it if it doesn't exist. 176 | 177 | sandbox = Kal.makeSandbox() unless sandbox exists 178 | 179 | Run the code and print the output (using `util.inspect`) or error trace. 180 | 181 | try 182 | _ = global._ 183 | returnValue = Kal.eval(code, {filename: 'repl', modulename: 'repl', bare:yes, sandbox:sandbox}) 184 | if returnValue is undefined 185 | global._ = _ 186 | repl.output.write "#{inspect(returnValue, no, 2, enableColors)}\n" 187 | catch err 188 | error err 189 | repl.prompt() 190 | 191 | Set up `stdin`. 192 | 193 | if stdin.readable and stdin.isRaw 194 | 195 | Handle piped input. 196 | 197 | pipedInput = '' 198 | repl = {} 199 | repl.prompt = -> 200 | stdout.write me._prompt 201 | repl.setPrompt = (p) -> 202 | me._prompt = p 203 | repl.input = stdin 204 | repl.output = stdout 205 | repl.on = -> 206 | return 207 | 208 | stdin.on 'data', (chunk) -> 209 | pipedInput += chunk 210 | nlre = /\n/ 211 | return unless nlre.test pipedInput 212 | lines = pipedInput.split "\n" 213 | pipedInput = lines[lines.length - 1] 214 | for line in lines.slice(1,-1) 215 | if line 216 | stdout.write "#{line}\n" 217 | run line, sandbox 218 | return 219 | 220 | stdin.on 'end', -> 221 | for line in pipedInput.trim().split("\n") 222 | if line 223 | stdout.write "#{line}\n" 224 | run line, sandbox 225 | stdout.write "\n" 226 | process.exit(0) 227 | 228 | else 229 | 230 | Handle user input using autocomplete and a read buffer. 231 | 232 | if readline.createInterface.length < 3 233 | repl = readline.createInterface stdin, autocomplete 234 | stdin.on 'data', (buffer) -> 235 | repl.write buffer 236 | else 237 | repl = readline.createInterface stdin, stdout, autocomplete 238 | 239 | Default multiline mode to off. 240 | 241 | multilineMode = off 242 | 243 | Handle the multi-line mode key switch (Ctrl-V). 244 | 245 | repl.input.on 'keypress', (char, key) -> 246 | return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v' 247 | cursorPos = repl.cursor 248 | repl.output.cursorTo 0 249 | repl.output.clearLine 1 250 | multilineMode = not multilineMode 251 | repl._line() if not multilineMode and backlog 252 | backlog = '' 253 | 254 | Switch the prompt and reset the cursor to the next line. 255 | 256 | newPrompt = REPL_PROMPT_MULTILINE when multilineMode otherwise REPL_PROMPT 257 | repl.setPrompt newPrompt 258 | repl.prompt() 259 | repl.cursor = cursorPos 260 | repl.output.cursorTo newPrompt.length + (repl.cursor) 261 | 262 | Handle Ctrl-d press at end of last line in multiline mode 263 | 264 | repl.input.on 'keypress', (char, key) -> 265 | return unless multilineMode and repl.line 266 | return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'd' 267 | multilineMode = off 268 | repl._line() 269 | 270 | Watch for Ctrl-C and handle it gracefully if we are in the midle of a multiline entry. 271 | 272 | repl.on 'attemptClose', -> 273 | if multilineMode 274 | multilineMode = off 275 | repl.output.cursorTo 0 276 | repl.output.clearLine 1 277 | repl._onLine repl.line 278 | return 279 | if backlog or repl.line 280 | backlog = '' 281 | repl.historyIndex = -1 282 | repl.setPrompt REPL_PROMPT 283 | repl.output.write '\n(^C again to quit)' 284 | repl.line = '' 285 | repl._line (repl.line) 286 | else 287 | repl.close() 288 | 289 | Cleanup on close. 290 | 291 | repl.on 'close', -> 292 | repl.output.write '\n' 293 | repl.input.destroy() 294 | 295 | Run `run` when a line is entered. 296 | 297 | repl.on 'line', run 298 | 299 | Start with the default prompt and go. 300 | 301 | repl.setPrompt REPL_PROMPT 302 | repl.prompt() 303 | -------------------------------------------------------------------------------- /source/kal.litkal: -------------------------------------------------------------------------------- 1 | The Kal Compiler 2 | ---------------- 3 | 4 | Kal is a highly readable, easy-to-use language that compiles to JavaScript. It's designed to be asynchronous and can run both on [node.js](http://nodejs.org) and in the browser. Kal makes asynchronous programming easy and clean by allowing functions to pause and wait for I/O, replacing an awkward callback syntax with a clean, simple syntax. 5 | 6 | The Kal compiler is written in Literate Kal. As a result, a "binary" (JavaScript) version is required to compile this source. You can obtain the latest precompiled package from npm using `npm install -g kal` (may require `sudo` depending on your setup). Once you have Kal installed globally, you can use the following scripts: 7 | 8 | * `npm run-script make` - This will compile the `sources` directory (this source) into the `compiled` directory. 9 | * `npm test` - Run the test suite against the `compiled` directory. You must run `npm install` for this repository to install the developer dependencies first. 10 | * `npm run-script bootstrap` - Build `sources` using the globally installed Kal compiler, then rebuild `sources` using the compiled version of itself. This also runs tests. This script is used to verify the compiler before deployment to npm. 11 | 12 | Structure 13 | ========= 14 | 15 | The compiler uses several stages to compile Kal code. 16 | 17 | For Literate Kal files (like this one), the `literate` module strips out the leading spaces from code blocks and turns Markdown syntax (like this line) into comments. 18 | 19 | literate = require './literate' 20 | 21 | The first stage is the lexer, which turns the raw string output into an array of tokens of various types (such as `IDENTIFIER`, `STRING`, and `NUMBER`). 22 | 23 | lexer = require './lexer' 24 | 25 | The "sugar" stage handles syntactic sugar. This includes features that would be difficult to handle in the full parsing stage, such as function calls without parentheses, multiline lists, and CoffeeScript style function definitions. The result is a modified array of tokens that can be read by the parser. 26 | 27 | sugar = require './sugar' 28 | 29 | The parser stage is uses a recursive descent parser to step through the token array and create a tree of objects representing the structure of the code (an Abstract Syntax Tree). 30 | 31 | parser = require './parser' 32 | 33 | The generator stage turns the syntax tree into JavaScript by traversing the tree depth-first, asking each node to produce the JavaScript code that corresponds to its function. This adds methods to the classes in the parser for JavaScript generation. 34 | 35 | generator = require './generator' 36 | 37 | This file gets the version reported by `kal -v` by reading the `package.json` file. If we are running in a web context (like for the online demo), this might fail, so we just return `UNKNOWN`. 38 | 39 | try 40 | exports.VERSION = require('../package.json').version 41 | catch 42 | exports.VERSION = 'UNKNOWN' 43 | 44 | Compilation 45 | =========== 46 | 47 | The compile function takes a `code` parameter that can be a string or buffer. This is the raw Kal (or Literate Kal) code. It also takes an `options` object which _may_ contain the following properties (all optional): 48 | 49 | * `bare` - if true, the resulting JavaScript will not be wrapped in a function wrapper. Top-level variables will leak to the global scope. The default is false (code is wrapped in a function). 50 | * `literate` - compile as a Literate Kal file. The default is false (regular Kal). 51 | * `show_tokens` - if true, the resulting token array is printed to `stdout`. This is useful for debugging the compiler. The default is false. 52 | 53 | Other members of `options` are ignored. 54 | 55 | This function returns a string containing the compiled JavaScript including proper spacing and indentation. 56 | 57 | 58 | function compile(code, options) 59 | 60 | If code is passed as a buffer, there can be issues with Unicode characters (gh-108). As a result we convert it explicitly to a string and remove any trailing whitespace or newlines. 61 | 62 | code = code.toString().trim() 63 | 64 | Files are wrapped in a function to prevent leakage to the global scope by default. 65 | 66 | options = {bare:no} when options doesnt exist 67 | 68 | Run through the Literate module (if necessary), then tokenize with the lexer. The lexer returns tokens and comments seperately. The token array is then run through the `sugar` module to handle hard-to-parse features. 69 | 70 | try 71 | code = literate.translate code when options.literate 72 | token_rv = lexer.tokenize code 73 | raw_tokens = token_rv[0] 74 | comments = token_rv[1] 75 | tokens = sugar.translate_sugar raw_tokens, options, lexer.tokenize 76 | 77 | We call the `js` method to recursively generate JavaScript from the tree, then return the value. 78 | 79 | root_node = parser.parse tokens, comments, options 80 | return root_node.js options 81 | 82 | We want to throw a string here, otherwise the user won't see a useful error message at the terminal (just an error class name). 83 | 84 | catch compile_error 85 | throw compile_error.message or compile_error 86 | 87 | exports.compile = compile 88 | 89 | Running Scripts (Eval) 90 | ====================== 91 | 92 | The `eval` function (called `kal_eval` locally to avoid conflicting with JavaScript's built-in `eval`) compiles a string of Kal code and runs it in the specified environment. All options in the `options` argument are passed through to `compile`. The `options` argument can contain the following other (optional) settings: 93 | 94 | * `show_js` - print the output JavaScript of `compile` to the console. Useful for debugging the compiler. The default is false. 95 | * `sandbox` - if true, the code will be run in a separate (sandbox) environment with its own set of globals. If false or missing, the code will be run in the `global` context. Alternatively, you can specify an object that represents the global context in which the script should run. 96 | * `modulename` - Passed to `makeSandbox`. If specified, the name of the module used when running the script. Default is `eval`. 97 | * `filename` - Passed to `makeSandbox`. If specified, the name of the file used when running the script. Default is `eval`. 98 | 99 | Code passed to this function is run immediately. 100 | 101 | function kal_eval(code, options) 102 | 103 | Compile the code object. Make sure to disable exports for evaluated code. 104 | 105 | options = {} if options doesnt exist 106 | options.no_export = yes 107 | js = compile code, options 108 | 109 | Show the output JavaScript if the user asked us to. This is really just for compiler debugging. 110 | 111 | print js when options.show_js 112 | 113 | Node's `vm` module is used to run the script in a specified global context. This is the same technique CoffeeScript (and node) use to run scripts. 114 | 115 | vm = require 'vm' 116 | 117 | If the a sandbox option was specified, either use the specified object as the sandbox or create a new one based on the current globals if the parameter is just `true`. Otherwise, just use the current global context. 118 | 119 | if options.sandbox 120 | sandbox = exports.makeSandbox(options) if options.sandbox is yes otherwise options.sandbox 121 | else 122 | sandbox = global 123 | 124 | If the sandbox context is the `global` object, run the script in this context. Otherwise, run it with the sandbox. 125 | 126 | if sandbox is global 127 | if sandbox.module doesnt exist and sandbox.require doesnt exist 128 | sandbox = exports.makeSandbox sandbox, options 129 | return vm.runInThisContext js 130 | else 131 | return vm.runInContext js, sandbox 132 | 133 | Export as just `eval`. 134 | 135 | exports.eval = kal_eval 136 | 137 | Sandboxes 138 | ========= 139 | 140 | This function creates a sandbox with the specified options based on the current `global` object. It initializes the sandbox with the necessary require hooks and path variables. The `sandbox` argument can be a script context object (from the `vm` module), or just an object with global variables for a new context. If an object is passed through, this function will create the script context object automatically. This function uses the following (optional) values from the `options` argument: 141 | 142 | * `modulename` - If specified, the name of the module used when running the script. Default is `eval`. 143 | * `filename` - If specified, the name of the file used when running the script. Default is `eval`. 144 | 145 | This function returns a `vm` script context object suitable for use with `vm.runInContext`. 146 | 147 | function makeSandbox(sandbox, options) 148 | vm = require 'vm' 149 | path = require 'path' 150 | 151 | Create a sandbox (`vm` script context object) based on the global environment if one is not specified. Otherwise, check if the specified sandbox is already a script context object. If not, we create a script context object based for it. 152 | 153 | if sandbox doesnt exist 154 | sandbox = vm.createContext(global) 155 | else if not (sandbox instanceof vm.Script.createContext().constructor) but sandbox isnt global 156 | new_sandbox = vm.createContext(global) 157 | for k of sandbox 158 | new_sandbox[k] = sandbox[k] 159 | sandbox = new_sandbox 160 | 161 | Set the `__filename` and `__dirname` that the script will see at run-time. We also set up the `module` and `require` objects to mimic what the script would see if it was run with `node` from the command line. 162 | 163 | sandbox.__filename = options?.filename or 'eval' 164 | sandbox.__dirname = path.dirname sandbox.__filename 165 | Module = require 'module' 166 | _module = new Module(options?.modulename or 'eval') 167 | sandbox.module = _module 168 | _require = (path) -> 169 | return Module._load path, _module, true 170 | sandbox.require = _require 171 | _module.filename = sandbox.__filename 172 | for r in Object.getOwnPropertyNames(require) 173 | if r isnt 'paths' 174 | _require[r] = require[r] 175 | 176 | This is the same hack node and coffee currently uses for their own REPL. 177 | 178 | _module.paths = Module._nodeModulePaths process.cwd() 179 | _require.paths = _module.paths 180 | _require.resolve = (request) -> 181 | return Module._resolveFilename request, _module 182 | return sandbox 183 | 184 | exports.makeSandbox = makeSandbox 185 | 186 | Require Extentions 187 | ================== 188 | 189 | This segment adds extensions to node's `require` function for Kal and Literate Kal files so that you can just `require` a Kal file without having to compile it first (assuming your script has already run `require 'kal'`). 190 | 191 | if require.extensions 192 | extension = (module, filename) -> 193 | 194 | Check if this is a Literate Kal file. 195 | 196 | is_literate = require('path').extname(filename) in ['.litkal', '.md'] 197 | 198 | Read the file, then compile using the `compile` function above. Then use node's built-in compile function to compile the JavaScript. 199 | 200 | content = compile(require('fs').readFileSync(filename, 'utf8'),{filename:filename, literate:is_literate}) 201 | module._compile(content, filename) 202 | 203 | Add the extension for all appropriate file types. Don't overwrite `.md` in case CoffeeScript or something else is already using it. 204 | 205 | require.extensions['.kal'] = extension 206 | require.extensions['.litkal'] = extension 207 | require.extensions['.md'] = extension except when require.extensions['.md'] exists 208 | -------------------------------------------------------------------------------- /source/lexer.litkal: -------------------------------------------------------------------------------- 1 | The Kal Lexer 2 | ------------- 3 | 4 | This file is responsible for translating raw code (a long string) into an array of tokens that can be more easily parsed by the parser. 5 | 6 | Tokens 7 | ====== 8 | 9 | **Comments** are tokens that are not part of the code. These are returned seperately from the token array with line markers to indicate where they belong. Comments can be single line, starting with a `#` and ending at the end of a line. They do not need to be the first thing on the line (for example `x = 1 #comment` is valid). Comments can also be multiline, starting with a `###` and ending with either a `###` or end-of-file. 10 | 11 | token_types = [[/^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)?$)|^(?:[^\n\S]*#(?!##[^#]).*)+/, 'COMMENT'], 12 | 13 | ***Regex** tokens are regular expressions. Kal does not do much with the regex contents at this time. It just passes the raw regex through to JavaScript. 14 | 15 | [/^(\/(?![\s=])[^[\/\n\\]*(?:(?:\\[\s\S]|\[[^\]\n\\]*(?:\\[\s\S][^\]\n\\]*)*])[^[\/\n\\]*)*\/)([imgy]{0,4})(?!\w)/,'REGEX'], 16 | 17 | Numbers can be either in hex format (like `0xa5b`) or decimal/scientific format (`10`, `3.14159`, or `10.02e23`). There is no distinction between floating point and integer numbers. 18 | 19 | [/^0x[a-f0-9]+/i, 'NUMBER'], 20 | [/^[0-9]+(\.[0-9]+)?(e[+-]?[0-9]+)?/i, 'NUMBER'], 21 | 22 | Block strings are multilne strings with triple quotes that preserve indentation. 23 | 24 | [/^'''(?:[^'\\]|\\.)*'''/, 'BLOCKSTRING'], 25 | [/^"""(?:[^"\\]|\\.)*"""/, 'BLOCKSTRING'], 26 | 27 | Strings can be either single or double quoted. 28 | 29 | [/^'(?:[^'\\]|\\.)*'/, 'STRING'], 30 | [/^"(?:[^"\\]|\\.)*"/, 'STRING'], 31 | 32 | Identifiers (generally variable names), must start with a letter, `$`, or underscore. Subsequent characters can also be numbers. Unicode characters are supported in variable names. 33 | 34 | [/^[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*/, 'IDENTIFIER'], 35 | 36 | A newline is used to demark the end of a statement. Whitespace without other content between newlines is ignored. Multiple newlines in a row generate just one token. 37 | 38 | [/^\n([\f\r\t\v\u00A0\u2028\u2029 ]*\n)*\r*/, 'NEWLINE'], 39 | 40 | Whitespace is ignored by the parser (and thrown out by the lexer), but needs to exist to break up other tokens. We also include the line continuation character, a backslash followed by one newline and some whitespace. 41 | 42 | [/^[\f\r\t\v\u00A0\u2028\u2029 ]+/, 'WHITESPACE'], 43 | [/^\\[\f\r\t\v\u00A0\u2028\u2029 ]*\n[\f\r\t\v\u00A0\u2028\u2029 ]*/, 'WHITESPACE'] 44 | 45 | A literal is a symbol (like `=` or `+`) used as an operator. Some literals like `+=` can be two characters 46 | 47 | [/^[\<\>\!\=]\=/, 'LITERAL'], 48 | [/^[\+\-\*\/\^\=\.><\(\)\[\]\,\.\{\}\:\?]/, 'LITERAL']] 49 | 50 | Token Values 51 | ============ 52 | 53 | Token objects also contain semantic values that are used during parsing. For example, `+ =` and `+=` are both equivalent to `+=`. This object maps tokens types to a function that returns their semantic value. 54 | 55 | parse_token = 56 | 57 | `NUMBER` tokens are represented by their numeric value. This is used in constant folding in some cases. 58 | 59 | NUMBER: (text) -> 60 | return Number(text) 61 | 62 | `STRING` tokens need any newlines removed to support JavaScript syntax. 63 | 64 | STRING: (text) -> 65 | return text.replace /\r?\n\r?/g, '' 66 | 67 | `BLOCKSTRING` tokens need newlines and indentation replaced. We also need to replace the multi-quotes with a single quote. 68 | 69 | BLOCKSTRING: (text) -> 70 | 71 | Collapse triple quotes to single. Remove the first/last newline right next to the enclosing quotes. 72 | 73 | rv = text 74 | rv = rv.replace /^'''(\r?\n)?|(\r?\n)?\s*'''$/g, "'" 75 | rv = rv.replace /^"""(\r?\n)?|(\r?\n)?\s*"""$/g, '"' 76 | 77 | Figure out the indent level of the first line after the triple quote. 78 | 79 | first_indent = rv.match(/^['"]\s*/)[0] 80 | indent_length = first_indent.match(/['"]\s*/)[0].length - 1 81 | 82 | Replace leading spaces on all subsequent lines. 83 | 84 | rv = rv.replace new RegExp("\\n\\s{#{indent_length}}",'g'), "\n" 85 | rv = rv.replace new RegExp("^\"\\s{#{indent_length}}"), '"' 86 | rv = rv.replace new RegExp("^'\\s{#{indent_length}}"), "'" 87 | 88 | Escape newlines. 89 | 90 | rv = rv.replace /\r?\n\r?/g, '\\n' 91 | 92 | Remove first line whitespace. 93 | 94 | return rv 95 | 96 | `IDENTIFIER` tokens are just the same as their content. 97 | 98 | IDENTIFIER: (text) -> 99 | return text 100 | 101 | `NEWLINE` tokens have only one possible semantic value, so they are just set to empty to make printouts with `showTokens` cleaner. 102 | 103 | NEWLINE: (text) -> 104 | return '' 105 | 106 | `WHITESPACE` tokens all have the same semantic value, so they are just replaced with a single space. 107 | 108 | WHITESPACE: (text) -> 109 | return ' ' 110 | 111 | `COMMENT` tokens are trimmed and have their `#`s removed. Any JavaScript comment markers are also escaped here. 112 | 113 | COMMENT: (text) -> 114 | rv = text.trim() 115 | rv = rv.replace /^#*\s*|#*$/g, "" 116 | rv = rv.replace /\n[\f\r\t\v\u00A0\u2028\u2029 ]*#*[\f\r\t\v\u00A0\u2028\u2029 ]*/g, '\n * ' 117 | return rv.replace(/(\/\*)|(\*\/)/g, '**') 118 | 119 | `LITERAL` tokens have their spaces removed as noted above. 120 | 121 | LITERAL: (text) -> 122 | return text.replace(/[\f\r\t\v\u00A0\u2028\u2029 ]/, '') 123 | 124 | `REGEX` tokens just pass through. 125 | 126 | REGEX: (text) -> 127 | return text 128 | 129 | The Lexer Class 130 | =============== 131 | 132 | This class is initialized with a code string. It's `tokens` and `comments` members are then populated using the `tokenize` method (called automatically during initialization). 133 | 134 | class Lexer 135 | 136 | The `line_number` argument is optional and allows you to specify a line offset to start on. 137 | 138 | method initialize(code, line_number=1) 139 | me.code = code 140 | me.line = line_number 141 | me.indent = 0 142 | me.indents = [] 143 | me.tokenize() 144 | 145 | method tokenize() 146 | me.tokens = [] 147 | me.comments = [] 148 | last_token_type = null 149 | index = 0 150 | 151 | This loop will try each regular expression in `token_types` against the current head of the code string until one matches. 152 | 153 | while index < me.code.length 154 | chunk = me.code.slice(index) 155 | for tt in token_types 156 | regex = tt[0] 157 | type = tt[1] 158 | text = regex.exec(chunk)?[0] 159 | if text exists 160 | me.type = type 161 | break 162 | 163 | If there was no match, this is a bad token and we will abort compilation here. We only report up to the first 16 characters of the token in case it is very long. 164 | 165 | if text doesnt exist 166 | code = this.code.toString().trim() 167 | context_len = 16 when code.length >= 16 otherwise code.length 168 | fail with "invalid token '#{code.slice(index,index+context_len)}...' on line #{this.line}" when text doesnt exist 169 | 170 | Parse the semantic value of the token. 171 | 172 | val = parse_token[me.type](text) 173 | 174 | If we previously saw a `NEWLINE`, we check to see if the current indent level has changed. If so, we generate `INDENT` or `DEDENT` tokens as appropriate in the `handleIndentation` method. `COMMENT`-only lines are ignored since we want to allow arbitrary indentation on non-code lines. 175 | 176 | if last_token_type is 'NEWLINE' and type isnt 'COMMENT' 177 | me.handleIndentation type, text 178 | 179 | For comments, we create a special comment token and put it in `me.comments`. We mark if it is postfix (after code on a line) or prefix (alone before a line of code). This will matter when we generate JavaScript code and try to place these tokens back as JavaScript comments. Multiline comments are also marked for this same reason. 180 | 181 | if type is 'COMMENT' 182 | comment_token = {text:text, line:me.line, value:val, type:type} 183 | if last_token_type is 'NEWLINE' or last_token_type doesnt exist 184 | comment_token.post_fix = no 185 | else 186 | comment_token.post_fix = yes 187 | if val.match(/\n/) 188 | comment_token.multiline = yes 189 | else 190 | comment_token.multiline = no 191 | me.comments.push comment_token 192 | 193 | For non-comment tokens, we generally just add the token to `me.tokens`. We will skip `NEWLINE` tokens if it would cause multiple `NEWLINE`s in a row. 194 | 195 | The `soft` attribute indicates that the token was separated from the previous token by whitespace. This is used by the `sugar` module in some cases to determine whether this is a function call or not. For example `my_function () ->` could mean `my_function()(->)` or `my_function(->)`. Marking whether or not there was whitespace allows us to translate `my_function () ->` differently from `my_function() ->`. 196 | 197 | else 198 | unless type is 'NEWLINE' and me.tokens[me.tokens.length - 1]?.type is 'NEWLINE' 199 | me.tokens.push {text:text, line:me.line, value:val, type:type, soft: last_token_type is 'WHITESPACE'} 200 | 201 | Update our current index and the line number we are looking at. Line numbers are used for source maps and error messages. 202 | 203 | index += text.length 204 | me.line += text.match(/\n/g)?.length or 0 205 | last_token_type = type 206 | 207 | Add a trailing newline in case the user didn't. The parser needs this in some cases. 208 | 209 | me.tokens.push {text:'\n',line:me.line, value:'', type:'NEWLINE'} 210 | 211 | Clear up any remaining indents at the end of the file. 212 | 213 | me.handleIndentation 'NEWLINE', '' 214 | 215 | Remove the newline we added if it wasn't needed. 216 | 217 | me.tokens.pop() if me.tokens[me.tokens.length-1].type is 'NEWLINE' 218 | 219 | The `handleIndentation` method adds `INDENT` and `DEDENT` tokens as necessary. 220 | 221 | method handleIndentation(type, text) 222 | 223 | Get the current line's indentation. 224 | 225 | indentation = text.length if type is 'WHITESPACE' otherwise 0 226 | 227 | If indentation has changed, push tokens as appropriate. Note that we treat multiple indents (multiples of two spaces) as a single indent/dedent pair. We keep track of the indentation level of each indent separately in the `me.indents` array in case the code is inconsistent. 228 | 229 | if indentation > me.indent 230 | me.indents.push me.indent 231 | me.indent = indentation 232 | me.tokens.push {text:text, line:me.line, value:'', type:'INDENT'} 233 | else if indentation < me.indent 234 | 235 | We allow for multiple dedents on a single line by looping until indentation matches. 236 | 237 | while me.indents.length > 0 and indentation < me.indent 238 | me.indent = me.indents.pop() 239 | fail with 'indentation is misaligned on line ' + me.line if indentation > me.indent 240 | me.tokens.push {text:text, line:me.line, value:'', type:'DEDENT'} 241 | 242 | A misalignment is not parseable so we throw an error. 243 | 244 | fail with 'indentation is misaligned' if indentation isnt me.indent 245 | 246 | The Tokenizer 247 | ============= 248 | 249 | This function is the entry point for the compiler. It parses a code string using the lexer and returns the tokens and comments separately. 250 | 251 | function tokenize(code) 252 | lex = new Lexer(code) 253 | return [lex.tokens, lex.comments] 254 | exports.tokenize = tokenize 255 | -------------------------------------------------------------------------------- /source/literate.litkal: -------------------------------------------------------------------------------- 1 | Literate Kal 2 | ------------ 3 | 4 | This file translates Literate Kal files to regular Kal files for the compiler. Literate Kal files are [Markdown](daringfireball.net/projects/markdown/) files with embedded code blocks that contain Kal code. All code blocks (denoted by four leading spaces) are treated as source code while all other Markdown is translated to comments. 5 | 6 | function translate(literate_code) 7 | 8 | This function reads the Literate code line by line, building a new array of non-Literate (illiterate?) code. 9 | 10 | regular_kal_lines = [] 11 | last_line = '' 12 | in_code_block = no 13 | for line in literate_code.split '\n' 14 | 15 | If the line starts with four spaces and the previous line was blank or code, we keep the line but remove the spaces. Otherwise, we prepend a `# ` comment marker to make it a comment. 16 | 17 | if line.match(/^ /) and (last_line is '' or in_code_block) 18 | in_code_block = yes 19 | regular_kal_lines.push line.slice 4 20 | else 21 | in_code_block = no 22 | regular_kal_lines.push '# ' + line 23 | last_line = line 24 | 25 | The translated code is standard Kal. 26 | 27 | return regular_kal_lines.join '\n' 28 | 29 | exports.translate = translate 30 | -------------------------------------------------------------------------------- /source/parser.litkal: -------------------------------------------------------------------------------- 1 | The Kal Parser 2 | -------------- 3 | 4 | The parser takes an array of tokens generated by the lexer and creates an abstract syntax tree (AST) of sytax nodes such as _Expressions_, _If Statements_, and _Assignment Statements_. The actual grammar definition is defined in the `grammar` module. 5 | 6 | grammar = require './grammar' 7 | 8 | The grammar defines a root object (a Kal `File` if you inspect the `grammar` module) that we use to kick off parsing and create a tree. 9 | 10 | GrammarRoot = grammar.GrammarRoot 11 | Grammar = grammar.Grammar 12 | 13 | exports.Grammar = Grammar 14 | 15 | The `parse` function is the entry point for the compiler. It creates a token stream and parses it, returning the tree. 16 | 17 | function parse(tokens, comments, options) 18 | ts = new TokenStream(tokens, comments, options) 19 | AST = new GrammarRoot(ts) 20 | return AST 21 | 22 | exports.parse = parse 23 | 24 | The `TokenStream` class allows easy navigation through the array of tokens, keeping track of line numbers and comments. 25 | 26 | class TokenStream 27 | method initialize(tokens, comments, options) 28 | me.tokens = tokens 29 | me.comments = comments 30 | me.options = options 31 | me.goto_token 0 32 | 33 | Go to the next token and return it. 34 | 35 | method next() 36 | return me.goto_token me.index+1 37 | 38 | Go back one token and return it. 39 | 40 | method prev() 41 | return me.goto_token me.index-1 42 | 43 | Return the next token but stay at the current location in the stream. 44 | 45 | method peek(delta_index) 46 | me.goto_token me.index + delta_index 47 | token = me.current 48 | me.goto_token me.index - delta_index 49 | return token 50 | 51 | Go to a specific token. Generally this is not called directly; it is used by `next`, `prev`, and `peek`. 52 | 53 | method goto_token(index) 54 | me.index = index 55 | 56 | Return an `EOF` token if we ran out of tokens. 57 | 58 | if me.index > me.tokens.length - 1 59 | me.current = {type: 'EOF', text: '', line: 0, value: ''} 60 | else if me.index < 0 61 | 62 | Actually error out if we try to read before the beginning of the file. This aborts compilation with a compiler error. 63 | 64 | throw 'Parser Error: tried to read before beginning of file' 65 | else 66 | me.current = me.tokens[me.index] 67 | 68 | Utility properties for the parser nodes. 69 | 70 | me.type = me.current.type 71 | me.text = me.current.text 72 | me.value = me.current.value 73 | me.line = me.current.line 74 | return me.current 75 | -------------------------------------------------------------------------------- /source/sugar.litkal: -------------------------------------------------------------------------------- 1 | Kal Sugar 2 | ========= 3 | 4 | This module applies a couple of "syntactic sugar" pre-processing steps to Kal code before it goes to the compiler. These steps would be onerous to do during the parsing stage, but are generally easier to do on a token stream. Each function in this module takes an input token stream and returns a new, possibly modified one. 5 | 6 | Some sugar functions use the keyword list from the grammar, most notable the implicit parentheses for function calls. 7 | 8 | 9 | grammar = require './grammar' 10 | KEYWORDS = grammar.KEYWORDS 11 | RVALUE_OK = grammar.RVALUE_OK 12 | 13 | The entry point for this module is the `translate_sugar` function, which takes an input token stream and returns a modified token stream for use with the parser. It also takes an optional `options` parameter which may contain the following properties: 14 | 15 | * **show_tokens** - if true, this module will print the input token stream to the console. This is useful for debugging the compiler 16 | 17 | The function also takes a `tokenizer` argument which is a function that given a code string, returns an array with the first element being a token array and the second a comment token array. The Kal compiler uses the `tokenize` funtion in the `lexer` module for this argument. `tokenizer`, if present, is used to tokenize code embedded in double-quoted strings. If this argument is missing, double-quoted strings with embedded code blocks will be left as strings. 18 | 19 | function translate_sugar (tokens, options, tokenizer) 20 | 21 | The current sugar stages are: 22 | 23 | 1. **code\_in\_strings** - for double-quoted strings with embedded code blocks (`"1 + 1 = #{1 + 1}"`), this function tokenizes the code blocks and converts the string to the equivalent of `"1 + 1 = " + (1 + 1)`. 24 | 2. **clean** - removes whitespace 25 | 3. **multiline\_statements** - removes line breaks after commas on long statements 26 | 4. **multiline\_lists** - this function collapses list definitions that span muliple lines into a single line, though the tokens do still retain their original line numbers. 27 | 5. **no\_paren\_function\_calls** - adds parentheses around implicit function calls like `my_function 1, 2`. 28 | 6. **print\_statement** - converts calls to `print` to `console.log` 29 | 7. **coffee\_style\_functions** - converts functions with CoffeeScript syntax (`(a,b) -> return a + b`) to standard Kal function syntax. 30 | 31 | The output is a new token stream (array). 32 | 33 | out_tokens = coffee_style_functions print_statement noparen_function_calls multiline_statements multiline_lists clean code_in_strings tokens, tokenizer 34 | 35 | Debug printing of the token stream is enabled with the `show_tokens` option. 36 | 37 | if options?.show_tokens 38 | debug = [] 39 | for t in out_tokens 40 | if t.type is 'NEWLINE' 41 | debug.push '\n' 42 | else 43 | debug.push t.value or t.type 44 | console.log debug.join ' ' 45 | return out_tokens 46 | exports.translate_sugar = translate_sugar 47 | 48 | Code In Strings 49 | =============== 50 | 51 | This function allows support for double-quoted strings with embedded code, like: "x is #{x}". It uses the `tokenizer` argument (a function that converts a code string into a token array, like `lexer.tokenize`) to run the code blocks in the string through the lexer. The return value is the merged stream of tokens. 52 | 53 | function code_in_strings (tokens, tokenizer) 54 | 55 | We abort if there is no `tokenizer` provided and just don't translate the strings. 56 | 57 | return tokens when tokenizer doesnt exist 58 | 59 | The output is a new token array (we don't modify the original). 60 | 61 | out_tokens = [] 62 | for token in tokens 63 | 64 | For double-quoted strings, we search for code blocks like `"#{code}"`. The regex uses the non-greedy operator to avoid parsing `"#{block1} #{block2}"` as a single block. 65 | 66 | if token.type in ['STRING','BLOCKSTRING'] and token.value[0] is '"' 67 | rv = token.value 68 | r = /#{.*?}/g 69 | m = r.exec rv 70 | 71 | We generally must add parentheses around any string that gets broken up for code blocks (and it is always safe to do so). `soft` indicates that this was added by the `sugar` module, not the user. It's passed forward to no-paren function calls. 72 | 73 | add_parens = yes if m otherwise no 74 | out_tokens.push({text:'(', line:token.line, value:'(', type:'LITERAL', soft:yes}) when add_parens 75 | 76 | For each code block match, we first add a string token to the stream for all the constant text before the block start, then a `+`. 77 | 78 | while m 79 | new_token_text = rv.slice(0,m.index) + '"' 80 | out_tokens.push {text:new_token_text, line:token.line, value:new_token_text, type:'STRING'} 81 | out_tokens.push {text:'+', line:token.line, value:'+', type:'LITERAL'} 82 | 83 | Next we add the parsed version of the code block (a token array) generated by running the code through the lexer. If there is more than one token, this also needs to be in parentheses. 84 | 85 | new_tokens = tokenizer(rv.slice(m.index+2,m.index+m[0].length-1))[0] 86 | out_tokens.push({text:'(', line:token.line, value:'(', type:'LITERAL'}) when new_tokens.length isnt 1 87 | out_tokens = out_tokens.concat new_tokens 88 | out_tokens.push({text:')', line:token.line, value:')', type:'LITERAL'}) when new_tokens.length isnt 1 89 | 90 | Next we make a string out of any remaining text after the block in case this is the last match. If the loop exits here, it gets added to the token stream, otherwise we ignore it since the next iteration will take care of it. If the string is the empty string, we set it to blank since we don't want things like `"a is #{a}"` turning into `("a is " + a + "")` for asthetic reasons. 91 | 92 | rv = '"' + rv.slice(m.index+m[0].length) 93 | if rv is '""' 94 | rv = '' 95 | else 96 | out_tokens.push {text:'+', line:token.line, value:'+', type:'LITERAL'} 97 | 98 | Find the next code block if there is one. 99 | 100 | r = /#{.*?}/g 101 | m = r.exec rv 102 | 103 | If there wasn't a next code block, add the remaining string (if any) and close paren. 104 | 105 | out_tokens.push({text:rv, line:token.line, value:rv, type:'STRING'}) when rv isnt '' 106 | out_tokens.push({text:')', line:token.line, value:')', type:'LITERAL', soft:yes}) when add_parens 107 | else 108 | 109 | For anything other than a double-quoted string, just pass it through. 110 | 111 | out_tokens.push token 112 | return out_tokens 113 | 114 | Clean 115 | ===== 116 | 117 | Removes whitespace. It marks tokens that were followed by whitespace so that the later stages can detect the difference between things like `my_function(a) ->` and `my_function (a) ->`. 118 | 119 | function clean (tokens) 120 | out_tokens = [] 121 | for token in tokens 122 | if token.type isnt 'WHITESPACE' 123 | out_tokens.push token 124 | else if out_tokens.length > 0 125 | out_tokens[out_tokens.length - 1].trailed_by_white = yes 126 | return out_tokens 127 | 128 | Multiline Statements 129 | ==================== 130 | 131 | This function removes newlines and indentation after commas, allowing long lines of code to be broken up into multiple lines. Token line numbers are preserved for error reporting. 132 | 133 | function multiline_statements (tokens) 134 | out_tokens = [] 135 | last_token = null 136 | 137 | We keep track of whether or not we are on a continued line and how many indents we ignored. 138 | 139 | continue_line = no 140 | reduce_dedent = 0 141 | 142 | for token in tokens 143 | skip_token = no 144 | 145 | If we see a newline after a comma, remove it from the stream and mark that we are in line continuation mode. 146 | 147 | if last_token?.value in [','] and token.type is 'NEWLINE' 148 | continue_line = yes 149 | skip_token = yes 150 | 151 | In line continuation mode, ignore indents and dedents, but keep track of them. We exit line continuation mode when we see a `DEDENT` that brings back to even with the original line. 152 | 153 | else if continue_line 154 | if token.type is 'INDENT' 155 | skip_token = yes 156 | reduce_dedent += 1 157 | else if token.type is 'NEWLINE' 158 | skip_token = yes 159 | else if token.type is 'DEDENT' 160 | if reduce_dedent > 0 161 | reduce_dedent -= 1 162 | skip_token = yes 163 | if reduce_dedent is 0 164 | out_tokens.push {text:'\n', line:token.line, value:'',type:'NEWLINE'} 165 | else 166 | 167 | When exiting line continuation mode, we have to add back in the last `NEWLINE`. 168 | 169 | out_tokens.push last_token 170 | 171 | Add the token to the new stream unless we decided to skip it. 172 | 173 | out_tokens.push(token) unless skip_token 174 | last_token = token 175 | return out_tokens 176 | 177 | No-Paren Function Calls 178 | ======================= 179 | 180 | This stage converts implicit function calls (`my_function a, b`) to explicit ones (`my_function(a,b)`). `NOPAREN_WORDS` specify keywords that should not be considered as a first argument to a function call. For example, we don't want `x is a` to turn into `x(is(a))`, but we do want `x y z` to become `x(y(z))`. 181 | 182 | 183 | NOPAREN_WORDS = ['is','otherwise','except','else','doesnt','exist','exists','isnt','inherits', 184 | 'from','and','or','xor','in','when','instanceof','of','nor','if','unless', 185 | 'except','for','with','wait','task','fail','parallel','series','safe','but', 186 | 'bitwise','mod','second','seconds','while','until','at','to','into','as'] 187 | 188 | function noparen_function_calls (tokens) 189 | out_tokens = [] 190 | triggers = [] 191 | enclosure_depth = 0 192 | indentation = 0 193 | declaring_function = no 194 | 195 | 196 | The `closeout` helper will push as many dangling `)`s as necessary to the stream when a closeout trigger is reached. 197 | 198 | function closeout() 199 | trigger = triggers[triggers.length-1] 200 | while trigger?.indentation is indentation and trigger?.enclosure_depth >= enclosure_depth 201 | out_tokens.push {text:')', line:token.line, value:')', type:'LITERAL'} 202 | triggers.pop() 203 | trigger = triggers[triggers.length-1] 204 | 205 | for token at i in tokens 206 | last_token = tokens[i-1] 207 | last_last_token = tokens[i-2] 208 | next_token = tokens[i+1] 209 | add_auto_paren = yes 210 | 211 | If the last token was a keyword, we normally can't add implicit parens (ex: `for x`). However, we do need to account for the following cases: 212 | 213 | * Two tokens ago was a `.` (like `x.for a`) 214 | * The last token is a keyword but it is a valid r-value (`me x`). 215 | 216 | If these conditions are not all met, we can't use implicit parentheses here. 217 | 218 | if last_token?.value in KEYWORDS 219 | add_auto_paren = no unless last_last_token?.value is '.' or last_token?.value in RVALUE_OK 220 | 221 | Next, we verify that the previous token was callable. This is only true if the token was an `IDENTIFIER` (not reserved), a `]` (like `x["func"] a`), or a `)` (like `get_func(1) 1`). Don't auto-paren things like `) ->` since they are Coffee-Style functions. 222 | 223 | if last_token?.type isnt 'IDENTIFIER' 224 | add_auto_paren = no unless last_token?.value in [']', ')'] 225 | add_auto_paren = no if last_token?.value is ')' and token.value is '-' and next_token?.value is '>' 226 | 227 | Check that the current token isn't a no-paren word (make sure we are not looking at something like `x for`). 228 | 229 | add_auto_paren = no when token.value in NOPAREN_WORDS 230 | 231 | Check that the current token is not a literal (don't want `my_function * 2` to become `my_function(* 2)`). There are some exceptions for callable literals, for things like `f {x:1}`, `f [1]`, and `->`. Also, parens with whitespace are allowed. For example, `my_func (3+2), 1` should get implicit parens. 232 | 233 | if token.type is 'LITERAL' 234 | acceptable_literal = no 235 | acceptable_literal = yes when token.value is '{' 236 | acceptable_literal = yes when token.value in ['[','('] but last_token?.trailed_by_white 237 | acceptable_literal = yes when token.value is '-' and next_token?.value is '>' 238 | add_auto_paren = no unless acceptable_literal 239 | 240 | We also handle the special case of a function definition with a space between the function name and the argument list. We need to consider the case of something like `x.function a b` which should turn into `x.function(a(b))` 241 | 242 | declaring_function = yes when last_token?.value in ['function','task','method','class'] and last_last_token?.value isnt '.' 243 | declaring_function = yes when last_last_token?.value is '-' and last_token?.value is '>' 244 | add_auto_paren = no when declaring_function 245 | 246 | Check if a parenthesis is `soft`, meaning added by the sugar and not the user. 247 | 248 | add_auto_paren = no when token.value is '(' but not token.soft 249 | 250 | Don't want to add parentheses around `bitwise left` or `bitwise right`, but we also really don't want `left` and `right` to be no-paren words, otherwise `x left` would not translate to `x(left)`. These are really useful words, so we handle them in this special case to avoid this issue. 251 | 252 | add_auto_paren = no when last_token?.value in ['left','right'] and last_last_token?.value is 'bitwise' 253 | 254 | 255 | Same story for `delete item` and `delete items`. `item` is a useful word. 256 | 257 | add_auto_paren = no when last_token?.value in ['item','items'] and last_last_token?.value is 'delete' 258 | 259 | The `trigger` variable tells us what indentation and paren depth we expect to be at when we close the last implicit paren. 260 | 261 | trigger = triggers[triggers.length - 1] 262 | 263 | We keep track of indents and dedents so that we can do things like `my_func - > NEWLINE INDENT return 2 NEWLINE DEDENT` and have parentheses enclose the entire call. 264 | 265 | if token.type is 'INDENT' 266 | indentation += 1 267 | out_tokens.push token 268 | else if token.type is 'DEDENT' 269 | indentation -= 1 270 | 271 | A dedent back to the previous level where the paren was inserted indicates we should close out the paren. Dedents need to go before the paren due to parsing rules. 272 | 273 | out_tokens.push token 274 | closeout() 275 | 276 | Similarly, we keep track of parens/braces/brackets. We assume the code has no open/close mismatches since these will wind up being parse errors anyway. Openers can be part of implicit function calls (`my_func {x:1}`). 277 | 278 | else if token.value in ['(','{','['] 279 | if add_auto_paren 280 | out_tokens.push {text:'(', line:token.line, value:'(', type:'LITERAL'} 281 | triggers.push {enclosure_depth:enclosure_depth, indentation:indentation} 282 | enclosure_depth += 1 283 | out_tokens.push token 284 | else if token.value in [')','}',']'] 285 | enclosure_depth -= 1 286 | 287 | An implicit paren can be caused by a closing of a brace/bracket/paren like `x = (my_func a) + b`. 288 | 289 | closeout() if trigger exists and enclosure_depth < trigger.enclosure_depth 290 | out_tokens.push token 291 | 292 | It can also be closed out by a newline or a tail conditional like `x = 1 if b`. 293 | 294 | else if token.type is 'NEWLINE' 295 | unless declaring_function 296 | closeout() 297 | declaring_function = no 298 | out_tokens.push token 299 | else if token.value in ['if','unless','when','except'] 300 | closeout() 301 | out_tokens.push token 302 | 303 | If we need to insert a paren, do it here. 304 | 305 | else if add_auto_paren 306 | out_tokens.push {text:'(', line:token.line, value:'(', type:'LITERAL'} 307 | triggers.push {enclosure_depth:enclosure_depth, indentation:indentation} 308 | out_tokens.push token 309 | 310 | Close out all auto-parens unconditionally if the file is finished. 311 | 312 | else if token.type is 'EOF' 313 | while triggers.length > 0 314 | out_tokens.push {text:')', line:token.line, value:')', type:'LITERAL'} 315 | triggers.pop() 316 | out_tokens.push token 317 | 318 | Pass through the current token. If it was a dedent we already pushed it. 319 | 320 | else 321 | out_tokens.push token 322 | 323 | Close out in case the EOF is missing. 324 | 325 | while triggers.length > 0 326 | out_tokens.push {text:')', line:token.line, value:')', type:'LITERAL'} 327 | triggers.pop() 328 | return out_tokens 329 | 330 | Coffee-Style Functions 331 | ====================== 332 | 333 | This function converts CoffeeScript-style functions (`() ->`) to Kal syntax. 334 | 335 | function coffee_style_functions (tokens) 336 | out_tokens = [] 337 | last_token = null 338 | 339 | We need to track the token index since we look back several tokens in this stage. 340 | 341 | i = 0 342 | while i < tokens.length 343 | token = tokens[i] 344 | 345 | Look for a `->`. 346 | 347 | if last_token?.value is '-' and token?.value is '>' 348 | 349 | If we see the `->`, that means the current token is `>` and we already added the `-` to the new stream. We have to pop the `-` off the stream. 350 | 351 | out_tokens.pop() 352 | 353 | We create a new token stream fragment for this function header. 354 | 355 | new_tokens = [] 356 | 357 | Next we examine the last token in the stream. Since we just popped the `-`, this will either be a `)` if the definition is in the form `(args) ->` or something else if it doesn't specify arguments. 358 | 359 | t = out_tokens.pop() 360 | if t?.value is ')' 361 | 362 | If there are arguments here, keep popping until we hit the `(`, adding the argument tokens to the `new_tokens` stream. At the end of this loop, `new_tokens` will be the arguments passed (if any) without enclosing parens. 363 | 364 | while t?.value isnt '(' 365 | new_tokens.unshift t 366 | t = out_tokens.pop() 367 | 368 | Pass the closing paren. 369 | 370 | new_tokens.unshift t 371 | else 372 | 373 | If no arguments were specified, let new_tokens be `()` 374 | 375 | out_tokens.push t 376 | new_tokens.push {text:'(', line:token.line, value:'(', type:'LITERAL'} 377 | new_tokens.push {text:')', line:token.line, value:')', type:'LITERAL'} 378 | 379 | Prepend the `function` token to `new_tokens`, which currently has the arguments (if any) in parentheses. Then add it to the `out_tokens` stream. 380 | 381 | f_token = {text:'function', line:token.line, value:'function', type:'IDENTIFIER'} 382 | new_tokens.unshift f_token 383 | out_tokens = out_tokens.concat new_tokens 384 | else 385 | 386 | If we're not handling a Coffee-Style function, just pass tokens through. 387 | 388 | out_tokens.push token 389 | last_token = token 390 | i += 1 391 | return out_tokens 392 | 393 | Multiline Lists 394 | =============== 395 | 396 | This function converts list definitions that span multiple lines into a single line. Tokens retain their original line numbers. This supports lists and explicit map definitions (`{}`). 397 | 398 | This function is admittedly awful and needs rework. 399 | 400 | function multiline_lists (tokens) 401 | out_tokens = [] 402 | 403 | We need to track nested lists. 404 | 405 | list_depth = 0 406 | last_token_was_separator = no 407 | indent_depths = [] 408 | indent_depth = 0 409 | leftover_indent = 0 410 | for token in tokens 411 | skip_this_token = no 412 | 413 | We need to keep track of whether or not this token is eligible as a list item separator. 414 | 415 | token_is_separator = (token.type in ['NEWLINE','INDENT', 'DEDENT'] or token.value is ',') 416 | 417 | When we see a list start, we push to the list stack. 418 | 419 | if token.value is '[' or token.value is '{' 420 | list_depth += 1 421 | indent_depths.push indent_depth 422 | indent_depth = 0 423 | 424 | Likewise for a list end, we pop the stack. 425 | 426 | else if token.value is ']' or token.value is '}' 427 | list_depth -= 1 428 | leftover_indent = indent_depth 429 | indent_depth = indent_depths.pop() 430 | 431 | Keep track of the indentation level, looking for a token that returns us to the original indent. We continue to skip indents/dedents until this happens. Basically, we want to ignore indentation inside these multi-line definitions. Once back to original the indent level, we push in a `NEWLINE`. 432 | 433 | Note that none of this happens unless we are inside a list definition (all these flags are ignored). 434 | 435 | else if token.type is 'INDENT' 436 | indent_depth += 1 437 | if leftover_indent isnt 0 438 | leftover_indent += 1 439 | skip_this_token = yes 440 | out_tokens.push({text:'', line:token.line, value:'\n', type:'NEWLINE'}) if leftover_indent is 0 441 | else if token.type is 'DEDENT' 442 | indent_depth -= 1 443 | if leftover_indent isnt 0 444 | leftover_indent -= 1 445 | out_tokens.push({text:'', line:token.line, value:'\n', type:'NEWLINE'}) if leftover_indent is 0 446 | skip_this_token = yes 447 | 448 | Skip newlines inside of list definitions. 449 | 450 | else if token.type is 'NEWLINE' 451 | if leftover_indent isnt 0 452 | skip_this_token = yes 453 | else 454 | leftover_indent = 0 455 | 456 | if list_depth > 0 457 | 458 | The first token in a newline stretch gets turned into a comma 459 | 460 | if token_is_separator and not last_token_was_separator 461 | out_tokens.push {text:',', line:token.line, value:',', type:'LITERAL'} 462 | else 463 | out_tokens.push token unless token_is_separator or skip_this_token 464 | else 465 | out_tokens.push token unless skip_this_token 466 | last_token_was_separator = token_is_separator and (list_depth > 0) 467 | return out_tokens 468 | 469 | Print Statements 470 | ================ 471 | 472 | Convert `print` tokens to `console` `.` `log` tokens. 473 | 474 | function print_statement (tokens) 475 | new_tokens = [] 476 | for token in tokens 477 | if token.value is 'print' and token.type is 'IDENTIFIER' 478 | new_tokens.push {text:'print', line:token.line, value:'console', type:'IDENTIFIER'} 479 | new_tokens.push {text:'print', line:token.line, value:'.', type:'LITERAL'} 480 | new_tokens.push {text:'print', line:token.line, value:'log', type:'IDENTIFIER'} 481 | else 482 | new_tokens.push token 483 | return new_tokens 484 | -------------------------------------------------------------------------------- /tests/assignment_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'Assignment Statement', -> 2 | it 'should assign a value to an identifier', -> 3 | a = 1 4 | a.should.equal 1 5 | 6 | it 'should allow access to the global namespace without leaking', -> 7 | n = process.title 8 | n.should.equal 'node' 9 | 10 | it 'should allow assignment of functions to variables', -> 11 | f = (a,b) -> 12 | return a + b 13 | g = function (a,b) 14 | return a * b 15 | f(2,3).should.equal 5 16 | g(5,10).should.equal 50 17 | 18 | it 'should support compound assignment operators', -> 19 | i = 5 20 | i += 1 21 | i.should.equal 6 22 | i -= 2 23 | i.should.equal 4 24 | i *= 3 25 | i.should.equal 12 26 | i = i mod 5 27 | i.should.equal 2 28 | i /= 2 29 | i.should.equal 1 30 | 31 | it 'should not declare variables used in compound assignments', -> #gh-48 32 | errors = 0 33 | try 34 | p += 1 35 | catch e 36 | errors += 1 37 | try 38 | p -= 1 39 | catch e 40 | errors += 1 41 | try 42 | p *= 1 43 | catch e 44 | errors += 1 45 | try 46 | p /= 1 47 | catch e 48 | errors += 1 49 | errors.should.equal 4 50 | (p exists).should.be.false 51 | -------------------------------------------------------------------------------- /tests/classes.kal: -------------------------------------------------------------------------------- 1 | describe 'Classes and Inheritance', -> 2 | it 'should support definition of class types and instantiation of said types', -> 3 | class KalC 4 | method initialize(val) 5 | me.val = val 6 | method get_val() 7 | return me.val 8 | function other_thing() 9 | (me.val exists).should.be.false 10 | i = new KalC(6) 11 | j = new KalC 7 12 | i.val.should.equal 6 13 | i.get_val().should.equal 6 14 | j.val.should.equal 7 15 | j.get_val().should.equal 7 16 | KalC.other_thing() 17 | 18 | it 'should support multilevel inheritance and method overloading', -> 19 | class KalC 20 | method initialize(val) 21 | me.val = val 22 | method get_val() 23 | return me.val 24 | function other_thing() 25 | (me.val exists).should.be.false 26 | 27 | class Child inherits from KalC 28 | method initialize(val) 29 | KalC.prototype.constructor.apply this, [val + 1] #no support for super yet 30 | 31 | class GrandChild inherits from Child 32 | method new_thing(a) 33 | return a 34 | function other_thing() 35 | (me.val exists).should.be.false 36 | return 2 37 | k = new GrandChild(3) 38 | k.val.should.equal 4 39 | k.get_val().should.equal 4 40 | Child.other_thing() 41 | GrandChild.other_thing().should.equal 2 42 | k.new_thing(1).should.equal 1 43 | 44 | it 'should support the Javascript instanceof operator', -> 45 | class A 46 | method x() 47 | return 2 48 | class B inherits from A 49 | method y() 50 | return 1 51 | class C inherits from A 52 | method x() 53 | return 3 54 | class D inherits from B 55 | method z() 56 | return 4 57 | a = new A 58 | b = new B 59 | c = new C 60 | d = new D 61 | (b instanceof A).should.be.true 62 | (a instanceof B).should.be.false 63 | (c instanceof B).should.be.false 64 | (c instanceof A).should.be.true 65 | (d instanceof B).should.be.true 66 | (d instanceof A).should.be.true 67 | 68 | #Issue 6 69 | function p(x,y) 70 | return 1 if x instanceof y 71 | return 2 72 | 73 | p(b,A).should.equal 1 74 | p(a,B).should.equal 2 75 | 76 | n = 1 77 | n = 2 if a instanceof B 78 | n.should.equal 1 79 | n = 3 if b instanceof A 80 | n.should.equal 3 81 | 82 | it 'should support the super keyword', -> 83 | #Issue gh-10 84 | class X 85 | method initialize(a,b,c) 86 | me.a = a when a exists otherwise 99 87 | me.b = b 88 | me.c = c 89 | 90 | class Y inherits from X 91 | method initialize(a,b) 92 | super a, b, 10 93 | 94 | class Z inherits from X 95 | method initialize(a,b,c) 96 | super 97 | 98 | class OMEGA inherits from X 99 | method other() 100 | return 101 | 102 | x = new X 1, 2, 3 103 | y = new Y 8, 9 104 | z = new Z 10, 11, 12 105 | om = new OMEGA 106 | 107 | x.a.should.equal 1 108 | x.b.should.equal 2 109 | x.c.should.equal 3 110 | y.a.should.equal 8 111 | y.b.should.equal 9 112 | y.c.should.equal 10 113 | z.a.should.equal 10 114 | z.b.should.equal 11 115 | z.c.should.equal 12 116 | om.a.should.equal 99 117 | 118 | it 'should support tasks', (done) -> #gh-98 119 | class ZZZ 120 | method initialize() 121 | me.x = 1 122 | task my_task(y,z) 123 | return me.x + y + z 124 | 125 | class ZZZ2 inherits from ZZZ 126 | task task2() 127 | wait for v from me.my_task(2,3) 128 | return v 129 | 130 | zz = new ZZZ2() 131 | wait for a from zz.my_task(9,10) 132 | a.should.equal 20 133 | wait for b from zz.task2() 134 | b.should.equal 6 135 | done() 136 | 137 | it 'should support late binding', (done) -> #gh-104 138 | class YYY 139 | method initialize() 140 | me.x = 1 141 | method thing() 142 | return me.x 143 | 144 | class YYY2 inherits from YYY 145 | method initialize() 146 | me.x = 2 147 | 148 | method p(a) of YYY 149 | me.x = a 150 | 151 | a = new YYY() 152 | b = new YYY2() 153 | a.thing().should.equal 1 154 | b.thing().should.equal 2 155 | a.p(7) 156 | b.p(8) 157 | a.thing().should.equal 7 158 | b.thing().should.equal 8 159 | done() 160 | -------------------------------------------------------------------------------- /tests/comprehensions.kal: -------------------------------------------------------------------------------- 1 | describe 'List Comprehensions', -> 2 | it 'should support for expressions inside list definitions', -> 3 | a = [1,2,3,4] 4 | b = [x+1 for x in a] 5 | a.should.eql([1,2,3,4]) 6 | b.should.eql([2,3,4,5]) 7 | 8 | it 'should support for expressions with generators', -> 9 | class TestGen 10 | method initialize() 11 | me.i = 1 12 | method next() 13 | me.i += 1 14 | if me.i < 10 15 | return me.i 16 | t = new TestGen 17 | x = [y for y in t] 18 | x.should.eql([2,3,4,5,6,7,8,9]) 19 | 20 | it "should support for expressions over an object's properties", -> 21 | y = {a:1,b:2,c:3} 22 | z1 = [k for property k in y] 23 | z1.should.eql(['a','b','c']) 24 | z2 = [v for property value v in y] 25 | z2.should.eql([1,2,3]) 26 | z3 = ["#{k}: #{v}" for property k with value v in y] 27 | z3.should.eql(['a: 1','b: 2','c: 3']) 28 | 29 | it 'should support the `arguments` object', -> #gh-114 30 | function a() 31 | return [x+1 for x in arguments] 32 | a(1,2,3).should.eql [2,3,4] 33 | 34 | it 'should support list shortcut', -> 35 | [0 to 4].should.eql [0,1,2,3,4] 36 | [5 to 2].should.eql [5,4,3,2] 37 | [1 to 1].should.eql [1] 38 | 39 | it 'should support conditionals in list comprehensions', -> 40 | arr = [0 to 20] 41 | x = [i * 2 for i in arr if i < 2] 42 | x.should.eql [0,2] 43 | y = [i for i in arr except when i mod 2 isnt 0] 44 | y.should.eql [0,2,4,6,8,10,12,14,16,18,20] 45 | z = [i for i in arr if i ^ 0.5 in arr] 46 | z.should.eql [0,1,4,9,16] 47 | 48 | it 'should support conditionals in object comprehensions', -> 49 | obj = 50 | a: 1 51 | b: 2 52 | c: 3 53 | d: 4 54 | x = [k for property k in obj if k isnt 'b'] 55 | x.sort() 56 | x.should.eql ['a','c','d'] 57 | x = [v * 2 for property value v in obj unless v mod 2 is 1] 58 | x.sort() 59 | x.should.eql [4,8] 60 | x = [v for property k with value v in obj unless k is 'c'] 61 | x.sort() 62 | x.should.eql [1,2,4] 63 | 64 | it 'should support function calls in list comprehensions', -> 65 | function lister(a) 66 | return [1+a,2+a,3+a] 67 | x = [y * 2 for y in lister 3] 68 | x.should.eql [8,10,12] 69 | -------------------------------------------------------------------------------- /tests/delete.kal: -------------------------------------------------------------------------------- 1 | describe 'Delete Statement', -> 2 | it 'should support syntax to delete object properties', -> 3 | obj = {a:1,b:2,c:3,d:4} 4 | delete property a from obj 5 | obj.should.eql {b:2,c:3,d:4} 6 | delete property "b" from obj 7 | obj.should.eql {c:3,d:4} 8 | delete property ggggg from obj 9 | obj.should.eql {c:3,d:4} 10 | 11 | it 'should support syntax to delete array members', -> 12 | arr = [0 to 5] 13 | delete item 2 from arr 14 | arr.should.eql [0,1,3,4,5] 15 | delete items 1 to 2 from arr 16 | arr.should.eql [0,4,5] 17 | delete items 0 to 2 from arr 18 | arr.should.eql [] 19 | 20 | arr = [0 to 5] 21 | delete items 0 to 100 from arr 22 | arr.should.eql [] 23 | 24 | arr = [0 to 5] 25 | delete items 1,3,5 from arr 26 | arr.should.eql [0,2,4] 27 | 28 | arr = [0 to 9] 29 | delete items 0, 9 from arr 30 | arr.should.eql [1,2,3,4,5,6,7,8] 31 | 32 | arr = [0 to 9] 33 | p = [0,9,2] 34 | delete items p from arr 35 | arr.should.eql [1,3,4,5,6,7,8] 36 | -------------------------------------------------------------------------------- /tests/else_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'Else Statement', -> 2 | it 'should support a blocked style', -> 3 | a = 4 4 | if a is 4 5 | a.should.equal 4 6 | x = 4 7 | else 8 | require('should').fail('running false block for true expression') 9 | if a is 3 10 | require('should').fail('running true block for false expression') 11 | else 12 | a.should.equal 4 13 | x = 5 14 | x.should.equal 5 15 | 16 | it 'should support all synonyms (else, otherwise)', -> 17 | n = 5 18 | when n is 5 19 | n.should.equal 5 20 | otherwise 21 | require('should').fail('running false block for true expression') 22 | when n is 3 23 | require('should').fail('running true block for false expression') 24 | otherwise 25 | n.should.equal 5 26 | unless n is 3 27 | n.should.equal 5 28 | else 29 | require('should').fail('running false block for true expression') 30 | unless n is 5 31 | require('should').fail('running true block for false expression') 32 | else 33 | n.should.equal 5 34 | except when n is 10 35 | n.should.equal 5 36 | otherwise 37 | require('should').fail('running false block for true expression') 38 | except when n is 5 39 | require('should').fail('running true block for false expression') 40 | else 41 | n.should.equal 5 42 | -------------------------------------------------------------------------------- /tests/export.kal: -------------------------------------------------------------------------------- 1 | 2 | # We check module.exports to make sure these 3 | # get exported 4 | function test1() 5 | function innerTest() 6 | return 0 7 | return 1 8 | 9 | task test2() 10 | return 2 11 | 12 | class Test3 13 | method initialize() 14 | me.thing = 3 15 | method other() 16 | return 3.1 17 | 18 | test4 = 4 19 | 20 | method test5() of Test3 21 | return 5 22 | 23 | describe 'Exports', -> 24 | test6 = 3 25 | it 'should export top-level functions by default', (done) -> 26 | module.exports.test1().should.equal 1 27 | wait for x from module.exports.test2() 28 | x.should.equal 2 29 | done() 30 | 31 | it 'should export top-level classes by default', -> 32 | t = new module.exports.Test3() 33 | t.thing.should.equal 3 34 | 35 | it 'should export module level variables by default', -> 36 | module.exports.test4.should.equal 4 37 | 38 | it 'should not export inner functions or variables', -> 39 | (module.exports.test6 exists).should.be.false 40 | (module.exports.innerTest exists).should.be.false 41 | (module.exports.other exists).should.be.false 42 | -------------------------------------------------------------------------------- /tests/expressions.kal: -------------------------------------------------------------------------------- 1 | describe 'Expression', -> 2 | it 'should allow an expression as a statement', -> 3 | 5 + 5 4 | 11 5 | 3 6 | x = 5 7 | x 8 | x.should.equal 5 9 | 10 | it 'should allow unary and binary expressions', -> 11 | x = 1 12 | x.should.equal 1 13 | x = 5 - 2 14 | x.should.equal 3 15 | 16 | it 'should support all arithmetic operators (+,-,*,/,^)', -> 17 | (2+3).should.equal 5 18 | (5*2.5).should.equal 12.5 19 | (10.1 - 2.6).should.equal 7.5 20 | (5/2).should.equal 2.5 21 | (3^3).should.equal 27 22 | (4^0.5).should.equal 2 23 | 24 | it 'should support logical operators (and, or, xor, not, nor, is, isnt)', -> 25 | (yes and yes).should.be.true 26 | (yes and no).should.be.false 27 | (no and yes).should.be.false 28 | (no and no).should.be.false 29 | 30 | (yes or yes).should.be.true 31 | (yes or no).should.be.true 32 | (no or yes).should.be.true 33 | (no or no).should.be.false 34 | 35 | (yes xor yes).should.equal 0 36 | (yes xor no).should.equal 1 37 | (no xor yes).should.equal 1 38 | (no xor no).should.equal 0 39 | 40 | (not yes).should.be.false 41 | (not no).should.be.true 42 | 43 | (yes nor yes).should.be.false 44 | (yes nor no).should.be.false 45 | (no nor yes).should.be.false 46 | (no nor no).should.be.true 47 | 48 | (2 is 2).should.be.true 49 | (3 is 1).should.be.false 50 | (3 is '3').should.be.false 51 | ('3' is '3').should.be.true 52 | 53 | (2 isnt 2).should.be.false 54 | (3 isnt 1).should.be.true 55 | (3 isnt '3').should.be.true 56 | ('3' isnt '3').should.be.false 57 | 58 | 59 | it 'should support parenthetical expressions', -> 60 | x = 5 * (2 + 3) 61 | x.should.equal 25 62 | y = 5 * (2 + 3 * (4 + 5)) 63 | z = (6) 64 | y.should.equal 145 65 | z.should.equal 6 66 | 67 | it 'should support order of operations', -> 68 | x = 1 + 2 * 4 + 4 - 3 ^ 2 / 6 69 | x.should.equal 11.5 70 | 71 | it 'should support variable access', -> 72 | x = 3 73 | y = 4 74 | z = 5 75 | (x^2 + y^2).should.equal z^2 76 | 77 | it 'should support property access', -> 78 | w = {y:2,z:3} 79 | w.x = {a:5,b:6} 80 | w.x.c = function () 81 | return {g:7,h:8} 82 | (w.x.a + w.x.b + w.y + w.z + w.x.c().g + w.x.c().h).should.equal 31 83 | 84 | it 'should support the exisential operators', -> 85 | (x exists).should.be.false 86 | (x doesnt exist).should.be.true 87 | (x?).should.be.false 88 | (y exists).should.be.false 89 | (y doesnt exist).should.be.true 90 | 91 | y = {a:1} 92 | 93 | (y exists).should.be.true 94 | (y doesnt exist).should.be.false 95 | (y?).should.be.true 96 | 97 | (y?.z exists).should.be.false 98 | y.z = 3 99 | (y?.z exists).should.be.true 100 | (y?.z).should.equal 3 101 | require('should').not.exist(y?.v) 102 | (y?.b?.d?.r?).should.be.false 103 | 104 | it 'should support unary operators (new, not)', -> 105 | function test(arg1) 106 | me.p = arg1 107 | a = new Object 108 | a.should.eql {} 109 | b = new test 'tttt' 110 | b.p.should.equal 'tttt' 111 | 112 | (not true).should.be.false 113 | (not false).should.be.true 114 | (not true is not true).should.be.true #order of operations 115 | 116 | it 'should support array access', -> 117 | a = [1,'j',12,no,[2,'f']] 118 | a[0].should.equal 1 119 | a[1].should.equal 'j' 120 | a[2].should.equal 12 121 | a[3].should.be.false 122 | a[4][0].should.equal 2 123 | a[4][1].should.equal 'f' 124 | (a[5]?).should.be.false 125 | (a[3]?).should.be.true 126 | a?[4]?[0].should.equal 2 127 | require('should').not.exist(a?[6]?[3]) 128 | require('should').not.exist(b?[6]?[3]) 129 | 130 | it 'should support negative unary expressions', -> #Issue 1 131 | x = -3 132 | y = 0 133 | x.should.equal y-3 134 | z = 4 + -3 135 | z.should.equal 1 136 | (-1).should.equal -(not false) 137 | 138 | it 'should support comparison operators (<, >, is, isnt)', -> 139 | (3 is 3).should.be.true 140 | (3 is 2).should.be.false 141 | ('3' is 3).should.be.false 142 | (3 isnt 2).should.be.true 143 | (1 isnt 1).should.be.false 144 | (1 isnt '1').should.be.true 145 | (5 > 2).should.be.true 146 | (5 > 5).should.be.false 147 | (2 < 3).should.be.true 148 | (2 < 2).should.be.false 149 | 150 | it 'should support comparison operators (<=, >=, ==, !=)', -> #Issue 7 151 | (2 <= 4).should.be.true 152 | (2 <= 2).should.be.true 153 | (2 <= 1).should.be.false 154 | (2 >= 4).should.be.false 155 | (2 >= 2).should.be.true 156 | (2 >= 1).should.be.true 157 | (2 == 2).should.be.true 158 | (2 == 1).should.be.false 159 | (2 != 2).should.be.false 160 | (2 != 1).should.be.true 161 | 162 | it 'should support list operators (in, not in)', -> 163 | a = ['a','b','c','1',2,3] 164 | (2 in a).should.be.true 165 | ('a' in a).should.be.true 166 | (3 in a).should.be.true 167 | (7 in a).should.be.false 168 | (1 in a).should.be.false 169 | 170 | #Issue 5 171 | (3 not in a).should.be.false 172 | (7 not in a).should.be.true 173 | 174 | it 'should support map operators (of, not of)', -> 175 | b = {x:2,y:'bob',z:1} 176 | ('x' of b).should.be.true 177 | ('z' of b).should.be.true 178 | ('a' of b).should.be.false 179 | ('x' not of b).should.be.false 180 | ('z' not of b).should.be.false 181 | ('a' not of b).should.be.true 182 | 183 | it 'should support the use of reserved words as properties', -> #gh-26 184 | tobj = {} 185 | tobj['on'] = function (a) 186 | return 2 + a 187 | tobj['in'] = 3 188 | 189 | tobj.in.should.equal 3 190 | x = tobj.on 5 191 | x.should.equal 7 192 | 193 | it 'should support the `but` operator', -> #gh-83 194 | a = true 195 | b = true 196 | c = false 197 | (a but b).should.be.true 198 | (a but not c).should.be.true 199 | (a but b but not c).should.be.true 200 | (a but c).should.be.false 201 | 202 | it 'should support bitwise operators', -> #gh-71 203 | a = 0x55 204 | b = 0xaa 205 | c = 0x01 206 | d = 0x80 207 | (a bitwise or b).should.equal 0xff 208 | (a bitwise or d).should.equal 0xd5 209 | (a bitwise and b).should.equal 0x00 210 | (a bitwise and c).should.equal 0x01 211 | (a bitwise xor c).should.equal 0x54 212 | (bitwise not a).should.equal(-a - 1) 213 | 214 | it 'should support chaining exponents', -> #gh-82 215 | x = 2 ^ 3 ^ 4 216 | x.should.equal Math.pow(Math.pow(2,3),4) 217 | y = 2 ^ 4 * 3 ^ 4 218 | y.should.equal Math.pow(2,4) * Math.pow(3,4) 219 | z = 2 ^ 4 ^ 5 * 3 ^ 4 220 | z.should.equal Math.pow(Math.pow(2,4),5) * Math.pow(3,4) 221 | a = 2 ^ -2 + -3 ^ 3 222 | a.should.equal Math.pow(2, -2) + Math.pow(-3, 3) 223 | 224 | it 'should support bitwise shift operators', -> 225 | x = 0x55 226 | (x bitwise left 1).should.equal 0xaa 227 | (x bitwise right 1).should.equal 0x2a 228 | 229 | it 'should support `not in` syntax', -> #gh-107 230 | x = [1, 2, 3, 4] 231 | (5 not in x).should.be.true 232 | (2 not in x).should.be.false 233 | (1 in x but 7 not in x).should.be.true 234 | if 5 > 3 and 10 not in x 235 | s = yes 236 | else 237 | s = no 238 | s.should.be.true 239 | if 3 > 6 or 10 not in x 240 | s = yes 241 | else 242 | s = no 243 | s.should.be.true 244 | 245 | it 'should treat "is not" as "isnt"', -> #gh-112 246 | x = 3 247 | (x is not 3).should.be.false 248 | (x is not 4).should.be.true 249 | (true is not 5).should.be.true # this case would fail if we compile this as `true is (not 5)`. 250 | -------------------------------------------------------------------------------- /tests/for_in_to_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'For In To Statement', -> 2 | it 'should handle defined iteration values', -> 3 | i = 0 4 | for index in 0 to 4 5 | i.should.equal index 6 | i += 1 7 | i.should.equal 5 8 | a = 10 9 | b = 15 10 | i = 10 11 | for index in a to b 12 | i.should.equal index 13 | i += 1 14 | i.should.equal b+1 15 | 16 | it 'should handle defined reverse iteration', -> 17 | i = 4 18 | for index in 4 to 0 19 | i.should.equal index 20 | i -= 1 21 | i.should.equal -1 22 | a = 10 23 | b = 4 24 | i = 10 25 | for index in a to b 26 | i.should.equal index 27 | i -= 1 28 | i.should.equal b-1 29 | -------------------------------------------------------------------------------- /tests/for_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'For Statement', -> 2 | it 'should loop until the condition is false', -> 3 | i = 0 4 | iterable = ['this','is','a','test'] 5 | out_s = '' 6 | for s in iterable 7 | out_s += s 8 | i += 1 9 | i.should.equal 4 10 | out_s.should.equal 'thisisatest' 11 | 12 | it 'should not execute if the iterable is empty', -> 13 | for x in [] 14 | require('should').fail 'executed for loop on empty input' 15 | 16 | it 'should define and use the conditional in the appropriate scope', -> 17 | j = 1 18 | x = function () 19 | for k in [1,2,3,4] 20 | z = 1 21 | for j in [1,2,3,4,5,6] 22 | z = 1 23 | return k 24 | x().should.equal 4 25 | j.should.equal 6 26 | (k exists).should.equal.false 27 | 28 | it 'should only evaluate the iterable once', -> 29 | n = [1,2,3,4] 30 | y = function () 31 | n.push(100) 32 | return n 33 | i = 0 34 | for x in y() 35 | i += 1 36 | i.should.equal 5 37 | x.should.equal 100 38 | 39 | it 'should accept an expression as an iterable', -> 40 | s = "a,b,c,def" 41 | t = ',' 42 | y = '' 43 | for x in s.split t 44 | y += x 45 | y.should.equal "abcdef" 46 | 47 | it 'should support object property enumeration', -> 48 | i = 0 49 | iterable = {'this':1,'is':2,'a':3,'test':4} 50 | out_s = '' 51 | for s of iterable 52 | out_s += s 53 | i += iterable[s] 54 | i.should.equal 1+2+3+4 55 | out_s.should.equal 'thisisatest' 56 | 57 | it 'should support an index variable', (done) -> #gh-118 58 | sum1 = 0 59 | sum2 = 0 60 | for x at i in [2,3,4,5] 61 | sum1 += x 62 | sum2 += i 63 | i.should.equal 3 64 | sum1.should.equal 14 65 | sum2.should.equal 6 66 | sum1 = 0 67 | sum2 = 0 68 | for series x at j in [2,3,4,5] 69 | sum1 += x 70 | pause for 0.001*j seconds 71 | sum2 += j 72 | sum1.should.equal 14 73 | sum2.should.equal 6 74 | sum1 = 0 75 | sum2 = 0 76 | for parallel x at i in [2,3,4,5] 77 | sum1 += x 78 | pause for 0.001*(4-i) seconds 79 | sum2 += i 80 | sum1.should.equal 14 81 | sum2.should.equal 6 82 | done() 83 | 84 | it 'should support generators', -> # gh-45 85 | class TestGen 86 | method initialize() 87 | me.i = 1 88 | method next() 89 | me.i += 1 90 | if me.i < 10 91 | return me.i 92 | t = new TestGen 93 | a = [] 94 | for x from t 95 | a.push x 96 | a.should.eql [2 to 9] 97 | a = [] 98 | for x from 2 to 4 99 | a.push x 100 | a.should.eql [2 to 4] 101 | a = [] 102 | for x from 4 to 2 103 | a.push x 104 | a.should.eql [4 to 2] 105 | -------------------------------------------------------------------------------- /tests/functions.kal: -------------------------------------------------------------------------------- 1 | describe 'Functions', -> 2 | it 'should support function calls with optional parentheses', -> 3 | calls = 0 4 | function a(x) 5 | calls += 1 6 | x.should.equal 6 7 | function b(y) 8 | calls += 1 9 | require('should').exist y 10 | a 6 11 | g = 6 12 | a g 13 | b /ra/ 14 | h = [a,b] 15 | h[0] 6 16 | h[1] /rox/ 17 | calls.should.equal 5 18 | 19 | it 'should allow default arguments', -> #gh-96 20 | function default_args(x,y=2,z=3) 21 | return x + y - z 22 | default_args(1, 5, 2).should.equal 4 23 | default_args(1, 5).should.equal 3 24 | default_args(1).should.equal 0 25 | isNaN(default_args()).should.be.true 26 | 27 | class DefaultArgClass 28 | method initialize(name="Bob") 29 | me.name = name 30 | method get_person(age,last_name="Jones") 31 | return "#{me.name} #{last_name} #{age}" 32 | bob = new DefaultArgClass() 33 | joe = new DefaultArgClass("Joe") 34 | bob.name.should.equal "Bob" 35 | joe.name.should.equal "Joe" 36 | bob.get_person(21).should.equal "Bob Jones 21" 37 | joe.get_person(20,"Smith").should.equal "Joe Smith 20" 38 | 39 | it 'should allow default arguments for tasks', (done) -> #gh-96 40 | task default_task(x,y=2,z=3) 41 | return x + y - z 42 | wait for a from default_task(1, 5, 2) 43 | a.should.equal 4 44 | wait for a from default_task(1, 5) 45 | a.should.equal 3 46 | wait for a from default_task(1) 47 | a.should.equal 0 48 | wait for a from default_task() 49 | isNaN(a).should.be.true 50 | done() 51 | 52 | it 'should support coffee style functions as arguments without extra parens', -> #gh-38 53 | function x(f) 54 | return f() 55 | r = x () -> 56 | return 2 57 | r.should.equal 2 58 | r = x -> 59 | return 3 60 | r.should.equal 3 61 | -------------------------------------------------------------------------------- /tests/if_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'If Statement', -> 2 | it 'should support a blocked style', -> 3 | a = 4 4 | if a is 4 5 | a.should.equal 4 6 | x = 4 7 | if a is 3 8 | require('should').fail('running true block for false expression') 9 | x.should.equal 4 10 | 11 | it 'should support all synonyms (when, unless, except when)', -> 12 | n = 5 13 | when n is 5 14 | n.should.equal 5 15 | when n is 3 16 | require('should').fail('running true block for false expression') 17 | unless n is 3 18 | n.should.equal 5 19 | unless n is 5 20 | require('should').fail('running true block for false expression') 21 | except when n is 10 22 | n.should.equal 5 23 | except when n is 5 24 | require('should').fail('running true block for false expression') 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/literate.litkal: -------------------------------------------------------------------------------- 1 | Literate Kal Tests 2 | ------------------ 3 | 4 | These test cases check the compiler's support for Literate Kal files. 5 | 6 | Kal = require '../compiled/kal' 7 | 8 | This feature was originally introduced in [gh-110](https://github.com/rzimmerman/kal/issues/110). 9 | 10 | describe 'Literate Kal', -> # gh-110 11 | 12 | Basic support is tested by attempting to evaluate a short script that assigns the result of `5 + 5` to a variable `x` and checking the result. The code line is surrounded by Markdown. Failure to parse will throw an error and fail the test case. 13 | 14 | it 'should be supported', -> 15 | literate_code = "This is a Literate Kal file.\n\n x = 5 + 5\n\nThis is more comment text." 16 | result = Kal.eval literate_code, {sandbox:yes, bare:yes, literate:yes} 17 | result.should.equal 10 18 | -------------------------------------------------------------------------------- /tests/other.kal: -------------------------------------------------------------------------------- 1 | Kal = require '../compiled/kal' 2 | 3 | describe 'Other', -> 4 | it 'should be able to handle empty lines with arbitrary indentation', -> 5 | b = Kal.eval 'a = 4\n \nb = 2\nif a is 2\n b = 3\n\n b = 4\n\r\v\t \r\nb', {sandbox:yes,bare:yes} 6 | 7 | b.should.equal 2 8 | 9 | it 'should be able to handle trailing spaces on a line', -> 10 | a = Kal.eval 'a = 4 \nb = 2\t\r\na', {sandbox:yes,bare:yes} 11 | a.should.equal 4 12 | 13 | it "should be able to handle autoparenthesis around list expressions", -> #gh-35 14 | a = [1,2,3,4] 15 | b = [x+1 for x in a] 16 | a.should.eql [1,2,3,4] 17 | b.should.eql [2,3,4,5] 18 | 19 | it "should support coffee-style list definitions (no commas/flexible whitespace)", -> #gh-9 20 | a = [1, 2 21 | 3 22 | 4, 23 | 5] 24 | a.should.eql [1,2,3,4,5] 25 | 26 | #gh-46 27 | it "should support coffee-style object declarations", -> 28 | d = Kal.eval "d =\n x:2\n y:\n a:1\n b:\n n:'hey',m:'you'\n 'z':[1\n 2\n 3]\nd", {sandbox:yes,bare:yes} 29 | expect = {x:2,y:{a:1,b:{n:'hey',m:'you'}},z:[1,2,3]} 30 | expect.should.eql d 31 | 32 | #gh-47 33 | it 'should correctly support chained conditionals', -> 34 | t1 = {'callback':1,'is_else_if':yes,'isnt_else_if':no} 35 | if t1.callback isnt 5 but not t1.is_else_if 36 | require('should').fail('chained conditional failed') 37 | if t1.callback isnt 1 but not t1.is_else_if 38 | require('should').fail('chained conditional failed') 39 | if t1.callback isnt 5 but not t1.isnt_else_if 40 | a = 0 #ok, do nothing 41 | else 42 | require('should').fail('chained conditional failed') 43 | rv = "aaa" 44 | a = "test" 45 | t1.some_func = -> 46 | return 'stuff' 47 | rv += a + t1.some_func() when t1.callback exists 48 | rv.should.equal "aaateststuff" 49 | rv += a + t1.some_func() when t1.garbage exists 50 | rv.should.equal "aaateststuff" 51 | rv += a + a + t1.some_func() when (no) 52 | rv.should.equal "aaateststuff" 53 | 54 | #gh-41 55 | it 'should be able to handle function calls at the end of a file', -> 56 | b = Kal.eval 'function test(a)\n return a + 5\ntest 3', {sandbox:yes,bare:yes} 57 | b.should.equal 8 58 | 59 | #gh-28 60 | it 'should correctly add parentheses to strings with embedded code', -> 61 | function tester(a,b) 62 | return a + b 63 | c = "test" 64 | r = tester "this is a #{c}", " of strings" 65 | r.should.equal "this is a test of strings" 66 | 67 | #gh-33 68 | it 'should support the pass statement for blank statements', -> 69 | pass 70 | a = 1 71 | function tttt() 72 | pass 73 | tttt() 74 | pass 75 | a.should.equal 1 76 | 77 | it 'should alias print to console.log', -> 78 | print.should.equal console.log 79 | 80 | it 'should handle if statements after object definitions', -> #gh-72 81 | jen = 82 | name: 'Jen' 83 | age: 27 84 | clearance: 'None' 85 | 86 | if jen.age is 27 87 | x = 'Jen is 27' 88 | x.should.equal 'Jen is 27' 89 | 90 | it 'should handle functions as members of map definitions', -> #gh-80 91 | obj = 92 | name: 'object' 93 | fun: function () 94 | return 2 95 | other: 3 96 | obj.fun().should.equal 2 97 | obj.name.should.equal 'object' 98 | obj.other.should.equal 3 99 | obj = 100 | fun: function () 101 | return 5 102 | obj.fun().should.equal 5 103 | obj = 104 | name: 'object 3' 105 | fun: -> 106 | return 99 107 | other: 3 108 | obj.fun().should.equal 99 109 | obj.name.should.equal 'object 3' 110 | obj.other.should.equal 3 111 | 112 | it 'should not allow keywords as identifiers', -> #gh-36 113 | a = -> 114 | Kal.compile "undefined = 2\n", {sandbox:yes,bare:yes} 115 | b = -> 116 | Kal.compile "x = [a for if in x]\n", {sandbox:yes,bare:yes} 117 | c = -> 118 | Kal.compile "function true()\n return 2\n", {sandbox:yes,bare:yes} 119 | d = -> 120 | Kal.compile "this = 1\n", {sandbox:yes,bare:yes} 121 | e = -> 122 | Kal.compile "this.p = 2\n", {sandbox:yes,bare:yes} 123 | f = -> 124 | Kal.compile "x = [1]\nfor this in x\n y = 1\n", {sandbox:yes,bare:yes} 125 | a.should.throw() 126 | b.should.throw() 127 | c.should.throw() 128 | d.should.throw() 129 | e.should.not.throw() 130 | f.should.throw() 131 | 132 | it 'should allow `none` and `nothing` as values', -> #gh-106 133 | x = none 134 | y = nothing 135 | z = null 136 | (x exists).should.be.false 137 | (y exists).should.be.false 138 | (z exists).should.be.false 139 | (x is null).should.be.true 140 | (y is null).should.be.true 141 | (z is null).should.be.true 142 | 143 | it 'should handle international characters', -> #gh-108 144 | #not an exhaustive test, but fixes this particular issue 145 | publisher = /出版社:(.*)/ 146 | ("出版社:ish".match(publisher) exists).should.be.true 147 | a = -> 148 | c = "publisher = /出版社:(.*)/" 149 | Kal.eval c, {sandbox:yes,bare:yes} 150 | Kal.compile c, {sandbox:yes,bare:yes} 151 | Kal.compile Buffer(c), {sandbox:yes,bare:yes} 152 | a.should.not.throw() 153 | 154 | it 'should support multiline strings', -> #gh-111 155 | "a 156 | b".should.equal "a b" 157 | """ 158 | a 159 | b 160 | """.should.equal "a\nb" 161 | ''' 162 | a 163 | b 164 | c 165 | d 166 | '''.should.equal "a\n b\nc\n d" 167 | d = 5 168 | """ 169 | a 170 | b 171 | c 172 | #{d} 173 | """.should.equal "a\n b\nc\n 5" 174 | 175 | it 'should support line continuation', -> #gh-62 176 | x = \ 177 | 3 178 | x.should.equal 3 179 | y = 10 + \ 180 | 2 181 | y.should.equal 12 182 | function t(a,b) 183 | return a \ 184 | + \ 185 | b 186 | t( \ 187 | 5, \ 188 | 1 \ 189 | ).should.equal 6 190 | r = t \ 191 | 8, \ 192 | 12 193 | r.should.equal 20 194 | 195 | it 'should correctly parse nested implicit paren calls with other literals', -> #gh-123 196 | function donothing(v) 197 | return v 198 | a = 2 ^ (3 if no otherwise 5) 199 | b = donothing(2 ^ (3 if no otherwise 5)) 200 | c = donothing 2 ^ ((3) if no otherwise 5) # this case was failing 201 | a.should.equal 32 202 | b.should.equal 32 203 | c.should.equal 32 204 | a = 2 ^ (3 if yes otherwise 5) 205 | b = donothing(2 ^ (3 if yes otherwise 5)) 206 | c = donothing 2 ^ ((3) if yes otherwise 5) 207 | a.should.equal 8 208 | b.should.equal 8 209 | c.should.equal 8 210 | 211 | it 'should handle nested implicit calls', -> 212 | function a(x) 213 | return 2 * x 214 | b = a a 1 215 | b.should.equal 4 216 | -------------------------------------------------------------------------------- /tests/parallel_block.kal: -------------------------------------------------------------------------------- 1 | describe 'Parallel Block', -> 2 | it 'should accept wait fors in a parallel block', (done) -> 3 | task callbacker1 (arg1) 4 | return arg1 + 1 5 | task callbacker2 (arg1) 6 | return arg1 + 2 7 | run in parallel 8 | wait for x from callbacker1 5 9 | wait for y from callbacker2 5 10 | x.should.equal 6 11 | y.should.equal 7 12 | done() 13 | 14 | it 'should execute tasks in parallel', (done) -> 15 | cb1_done = no 16 | cb2_done = no 17 | task cb1() 18 | until cb2_done 19 | pause for 0.001 seconds 20 | cb1_done = cb2_done 21 | return 2 22 | task cb2() 23 | pause for 0.005 seconds 24 | cb2_done = yes 25 | return 1 26 | run in parallel 27 | wait for a from cb1() 28 | wait for b from cb2() 29 | a.should.equal 2 30 | b.should.equal 1 31 | cb1_done.should.be.true 32 | cb2_done.should.be.true 33 | done() 34 | 35 | it 'should support tasks with no return values', (done) -> 36 | cb1_done = no 37 | cb2_done = no 38 | task cb1() 39 | until cb2_done 40 | pause for 0.001 seconds 41 | cb1_done = cb2_done 42 | return 2 43 | task cb2() 44 | pause for 0.005 seconds 45 | cb2_done = yes 46 | return 1 47 | run in parallel 48 | wait for cb1() 49 | wait for cb2() 50 | cb1_done.should.be.true 51 | cb2_done.should.be.true 52 | done() 53 | 54 | it 'should support `safe` and error handling', (done) -> 55 | task cb1() 56 | throw 'ish' 57 | task cb2() 58 | return 2 59 | error_happened = no 60 | try 61 | run in parallel 62 | wait for x from cb1() 63 | wait for y from cb2() 64 | catch error 65 | error_happened = yes 66 | error[0].should.equal 'ish' 67 | (error[1] doesnt exist).should.be.true 68 | error_happened.should.be.true 69 | y.should.equal 2 70 | y = 0 71 | run in parallel 72 | safe wait for x from cb1() 73 | wait for y from cb2() 74 | x.should.equal 'ish' 75 | y.should.equal 2 76 | done() 77 | 78 | it 'should consider the `wait for` prefix optional', (done) -> 79 | x = 0 80 | task c1() 81 | x += 1 82 | return 1 83 | task c2() 84 | return 2, 5 85 | task c3() 86 | return 3, 4 87 | run in parallel 88 | c1() 89 | wait for y, yy from c2() 90 | z, zz from c3() 91 | safe wait for c1() 92 | x.should.equal 2 93 | y.should.equal 2 94 | yy.should.equal 5 95 | z.should.equal 3 96 | zz.should.equal 4 97 | done() 98 | -------------------------------------------------------------------------------- /tests/regex.kal: -------------------------------------------------------------------------------- 1 | describe 'regex', -> 2 | it 'should parse regular expressions and execute them using native JS', -> 3 | r = /[0-9]+/g 4 | s = '43 4 test 533 x'.replace r, 'o' 5 | s.should.equal 'o o test o x' 6 | 7 | -------------------------------------------------------------------------------- /tests/return_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'Return Statement', -> 2 | it 'should support no arguments and return undefined', -> 3 | r = function () 4 | return 5 | 6 | require('should').not.exist(r()) 7 | 8 | it 'should support one argument (an expression) and return the argument', -> 9 | z = function () 10 | return 5 11 | y = function () 12 | return 2 + 3 * 5 13 | z().should.equal 5 14 | y().should.equal 17 15 | 16 | it 'should stop execution', -> 17 | p = function () 18 | a = 1 19 | return a 20 | throw('return did not stop execution') 21 | p().should.equal 1 22 | 23 | it 'should work conditionally', -> 24 | g = function () 25 | a = 9 26 | return a when a is 12 27 | a = 3 28 | return a when a is 3 29 | g().should.equal 3 30 | 31 | it 'should work conditionally when bare', -> 32 | a = 0 33 | g = function () 34 | a = 9 35 | return unless a is 4 36 | a = 3 37 | g() 38 | a.should.equal 9 39 | -------------------------------------------------------------------------------- /tests/strings.kal: -------------------------------------------------------------------------------- 1 | describe 'Strings', -> 2 | it 'should parse and compile code in #{} blocks in double-quoted strings', -> #Issue 4 3 | d = 'e' 4 | ("abc#{d}").should.equal 'abce' 5 | function g(n) 6 | return n + 1 7 | ("xyz #{d+d} #{d exists} #{f exists} #{g(3)}").should.equal 'xyz ee true false 4' 8 | it 'should support single quoted strings', -> 9 | #gh-27 10 | r = '\n' 11 | r.should.equal "\n" 12 | s = '\n(press a key)' 13 | s.should.equal "\n(press a key)" 14 | x = {} 15 | x["on"] = function (a,b) 16 | return a + b 17 | y = x.on 'line', r 18 | #this comment has a ' in it 19 | y.should.equal "line\n" 20 | #this comment has a ' in it 21 | z = "\\" 22 | z.should.equal '\\' 23 | if z isnt '\\' 24 | require('should').fail("couldn't parse string with slashes") 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/timers.kal: -------------------------------------------------------------------------------- 1 | describe 'Timer Functions', -> 2 | it 'should provide an alias for setTimeout', (done) -> 3 | t = new Date() 4 | pause for 0.010 5 | delta = new Date() - t 6 | delta.should.be.above(8) 7 | done() 8 | 9 | it 'should allow the optional "second[s]" suffix', (done) -> 10 | t = new Date() 11 | pause for 0.006 second 12 | delta = new Date() - t 13 | delta.should.be.above(4) 14 | t = new Date() 15 | pause for 0.004 seconds 16 | delta = new Date() - t 17 | delta.should.be.above(2) 18 | done() 19 | 20 | it 'should allow expressions in the seconds clause', (done) -> 21 | t = new Date() 22 | a = 0.004 23 | pause for a + a seconds 24 | delta = new Date() - t 25 | delta.should.be.above(6) 26 | done() 27 | -------------------------------------------------------------------------------- /tests/trycatch.kal: -------------------------------------------------------------------------------- 1 | describe 'Try/Catch/Throw Statements', -> 2 | it 'should accept try catch blocks and catch throw errors', -> 3 | caught = no 4 | try 5 | x = 1 6 | y = 2 7 | throw 'c' 8 | z = 3 9 | catch e 10 | e.should.equal 'c' 11 | caught = yes 12 | caught.should.be.true 13 | (z exists).should.be.false 14 | x.should.equal 1 15 | y.should.equal 2 16 | 17 | it 'should work without a catch block (just ignore errors)', -> 18 | try 19 | x = 1 20 | y = 2 21 | throw 'c' 22 | z = 3 23 | (z exists).should.be.false 24 | x.should.equal 1 25 | y.should.equal 2 26 | 27 | it 'should fail to compile if there are callbacks in the catch clause (not supported)', -> 28 | Kal = require '../compiled/kal' 29 | thrown = no 30 | try 31 | b = Kal.eval 'function x()\n try\n x = 1\n catch e\n wait for x from callbackthing()\n', {sandbox:yes,bare:yes} 32 | catch e 33 | thrown = yes 34 | e.should.match /not supported/ig 35 | thrown.should.be.true 36 | 37 | -------------------------------------------------------------------------------- /tests/unpack.kal: -------------------------------------------------------------------------------- 1 | describe 'List and Object Unpacking', -> # gh-20 2 | it 'should support object unpacking', -> 3 | obj = 4 | a: 1 5 | b: 'stuff' 6 | c: 7 | c1: 'hello' 8 | d: 2 9 | unpack obj into a, b, c 10 | a.should.equal 1 11 | b.should.equal 'stuff' 12 | c.should.eql {c1: 'hello'} 13 | it 'should support the as statement when unpacking objects', -> 14 | obj = 15 | a: 1 16 | b: 'stuff' 17 | c: 18 | c1: 'hello' 19 | d: 2 20 | unpack obj into a, b as bee 21 | a.should.equal 1 22 | bee.should.equal 'stuff' 23 | 24 | it 'should support sub-objects', -> 25 | obj = 26 | a: 1 27 | b: 'stuff' 28 | c: 29 | c1: 'hello' 30 | d: 2 31 | unpack obj into a, c.c1 as c1 32 | a.should.equal 1 33 | c1.should.equal 'hello' 34 | -------------------------------------------------------------------------------- /tests/waitfor_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'Wait For Statement', -> 2 | it 'should accept single arguments', (done) -> 3 | task callbacker (arg1) 4 | return arg1 + 1 5 | wait for x from callbacker 5 6 | x.should.equal 6 7 | done() 8 | 9 | it 'should accept multiple arguments', (done) -> 10 | task callbacker2 (arg1, arg2, arg3) 11 | return arg1 + 1, arg2 - 1, arg3 + 2 12 | wait for x, y, z from callbacker2 5, 6, 7 13 | x.should.equal 6 14 | y.should.equal 5 15 | z.should.equal 9 16 | done() 17 | 18 | it 'should work inside of if statements', (done) -> 19 | task callbacker3(arg1) 20 | return arg1 * 2 21 | x = 2 22 | if x is 2 23 | wait for y from callbacker3 7 24 | n = 2 25 | else 26 | wait for z from callbacker3 8 27 | y.should.equal 14 28 | n.should.equal 2 29 | (z doesnt exist).should.be.true 30 | done() 31 | 32 | it 'should work inside of else if statements', (done) -> 33 | task callbacker3(arg1) 34 | return arg1 * 2 35 | x = 2 36 | if x is 5 37 | wait for y from callbacker3 7 38 | n = 2 39 | else if x is 2 40 | n = 4 41 | wait for z from callbacker3 8 42 | else 43 | n = 5 44 | z.should.equal 16 45 | n.should.equal 4 46 | (y doesnt exist).should.be.true 47 | done() 48 | 49 | it 'should work inside of nested if statements (1)', (done) -> 50 | task callbacker3(arg1) 51 | return arg1 * 2 52 | x = 5 53 | if x is 5 #true 54 | wait for y from callbacker3 7 55 | if y is 3 56 | y = 10 57 | n = 0 58 | else #true, it's 14 59 | n = 0 60 | wait for y from callbacker3 y 61 | n = 0 62 | y += 1 #28+1=29 63 | n = 2 64 | else if x is 2 65 | n = 4 66 | wait for z from callbacker3 8 67 | else 68 | n = 5 69 | if yes 70 | wait for p from callbacker3 1 71 | y.should.equal 29 72 | n.should.equal 2 73 | (p doesnt exist).should.be.true 74 | (z doesnt exist).should.be.true 75 | done() 76 | 77 | it 'should work inside of nested if statements (2)', (done) -> 78 | task callbacker4(arg1) 79 | return arg1 * 2 80 | x = 1 81 | if x is 5 82 | wait for y from callbacker4 7 83 | if y is 3 84 | y = 10 85 | n = 0 86 | else #true, it's 14 87 | n = 0 88 | wait for y from callbacker4 y 89 | n = 0 90 | y += 1 #28+1=29 91 | n = 2 92 | else if x is 2 93 | n = 4 94 | wait for z from callbacker4 8 95 | else #true 96 | n = 5 97 | if yes 98 | wait for p from callbacker4 1 99 | p.should.equal 2 100 | n.should.equal 5 101 | (t doesnt exist).should.be.true 102 | (z doesnt exist).should.be.true 103 | done() 104 | 105 | it 'should work inside of nested if statements (3)', (done) -> 106 | task callbacker5(arg1) 107 | return arg1 * 3 108 | x = 2 109 | y = 3 110 | if x is 5 111 | wait for z from callbacker5 7 112 | if y is 3 113 | z = 10 114 | n = 0 115 | else 116 | n = 1 117 | wait for y from callbacker5 y 118 | n = 0 119 | y += 1 120 | n = 2 121 | else if x is 2 #true 122 | n = 4 123 | if y isnt 3 124 | z = 10 125 | else #true 126 | if x is 2 # true 127 | wait for z from callbacker5 8 128 | z += 1 129 | if z is 25 # true 130 | wait for z from callbacker5 z 131 | else #true 132 | n = 5 133 | if yes 134 | wait for p from callbacker4 1 135 | z.should.equal 75 136 | n.should.equal 4 137 | done() 138 | 139 | it 'should work inside of for parallel in loops', (done) -> 140 | task callbacker3(arg1) 141 | return arg1 * 2 142 | output = [] 143 | for parallel i in [1,2,3,4] 144 | wait for j from callbacker3 i 145 | output.push j 146 | expected = [2,4,6,8] 147 | 148 | for o in output 149 | (o in expected).should.be.true 150 | done() 151 | 152 | it 'should only call back once from for parallel loops', (done) -> # gh-63 153 | function callbacker99(next) 154 | setTimeout next, 0 155 | counter = 0 156 | task callbacker98() 157 | for parallel i in [1,2,3,4] 158 | wait for ish from callbacker99() 159 | counter += 1 160 | return counter 161 | wait for c from callbacker98() 162 | c.should.equal 4 163 | done() 164 | 165 | 166 | it 'should work inside of for serial in loops', (done) -> 167 | task callbacker3(arg1) 168 | return arg1 * 2 169 | output = [] 170 | for series i in [1,2,3,4] 171 | wait for j from callbacker3 i 172 | output.push j 173 | expected = [2,4,6,8] 174 | 175 | output.should.eql expected 176 | done() 177 | 178 | it 'should handle nested loops (1)', (done) -> 179 | task shrinker(i) 180 | return i - 9 181 | task decrementer(i) 182 | return i - 1 183 | grids = [[[1,2,3],[4,5,6],[7,8,9]],[[10,11,12],[13,14,15],[16,17,18]]] 184 | sum = 0 185 | for parallel grid in grids 186 | grid.length.should.equal 3 187 | for series row in grid 188 | row.length.should.equal 3 189 | for parallel column in row 190 | if column > 9 191 | wait for icolumn from shrinker(column) 192 | sum += icolumn 193 | else 194 | sum += column 195 | sum.should.equal 90 196 | done() 197 | 198 | it 'should handle nested loops (2)', (done) -> 199 | task shrinker(i) 200 | return i - 9 201 | task decrementer(i) 202 | return i - 1 203 | grids = [[[1,2,3],[4,5,6],[7,8,9]],[[10,11,12],[13,14,15],[16,17,18]]] 204 | sum = 0 205 | for series grid in grids 206 | grid.length.should.equal 3 207 | for series row in grid 208 | row.length.should.equal 3 209 | for series column in row 210 | if column > 9 211 | wait for icolumn from shrinker(column) 212 | else 213 | icolumn = column 214 | while icolumn > 0 215 | wait for icolumn from decrementer(icolumn) 216 | sum += 1 217 | sum.should.equal 90 218 | done() 219 | 220 | 221 | it 'should handle nested loops (3)', (done) -> 222 | task shrinker(i) 223 | return i - 9 224 | task decrementer(i) 225 | return i - 1 226 | grids = [[[1,2,3],[4,5,6],[7,8,9]],[[10,11,12],[13,14,15],[16,17,18]]] 227 | sum = 0 228 | g = 5 229 | if g is 5 230 | wait for g from decrementer(g) 231 | g.should.equal 4 232 | for parallel grid in grids 233 | grid.length.should.equal 3 234 | if grid[0][0] isnt 1 235 | for series row in grid 236 | row.length.should.equal 3 237 | for parallel column in row 238 | if column > 9 239 | wait for icolumn from shrinker(column) 240 | sum += icolumn 241 | else 242 | sum += column 243 | else 244 | wait for x from decrementer(grid.length) 245 | sum += x 246 | sum.should.equal 45+3-1 247 | done() 248 | 249 | it 'should support try/catch statements in callback loops', (done) -> 250 | task shrinker(i) 251 | return i - 9 252 | task decrementer(i) 253 | return i - 1 254 | grids = [[[1,2,3],[4,5,6],[7,8,9]],[[10,11,12],[13,14,15],[16,17,18]]] 255 | sum = 0 256 | for parallel grid in grids 257 | grid.length.should.equal 3 258 | try 259 | for series row in grid 260 | row.length.should.equal 3 261 | for parallel column in row 262 | wait for icolumn from decrementer(column) 263 | if icolumn >= 9 264 | fail with "TOO BIG" 265 | else 266 | sum += icolumn + 1 267 | catch e 268 | sum += 1 269 | sum.should.equal 45 + 9 #because the inner loop is parallel, all 9 items will throw errors, it will not exit early 270 | done() 271 | 272 | 273 | it 'should work inside of while loops', (done) -> 274 | task callbacker3(arg1) 275 | return arg1 * 2 276 | output = [] 277 | i = 1 278 | while i <= 4 279 | wait for j from callbacker3 i 280 | output.push j 281 | i += 1 282 | expected = [2,4,6,8] 283 | 284 | output.should.eql expected 285 | done() 286 | 287 | it 'should throw when called back with an error from a task', (done) -> 288 | task callbacker10(arg1) 289 | fail with "error!!!" 290 | error_happened = no 291 | try 292 | wait for x from callbacker10 5 293 | catch e 294 | e.should.equal "error!!!" 295 | error_happened = yes 296 | error_happened.should.be.true 297 | done() 298 | 299 | it 'should handle consecutive try/catch blocks', (done) -> #gh-94 300 | d = 0 301 | task trytry(a,b,c) 302 | d = a + b + c 303 | return 0 304 | function errorizor(next) 305 | return next "ERR", 2 306 | wait for r from trytry 1, 2, 3 307 | d.should.equal 6 308 | r.should.equal 0 309 | error_happened = no 310 | try 311 | wait for v from errorizor() 312 | catch e 313 | error_happened = yes 314 | e.should.equal "ERR" 315 | error_happened.should.be.true 316 | try 317 | safe wait for err, v from errorizor() 318 | catch e 319 | require('should').fail 'did not expect an error' 320 | err.should.equal "ERR" 321 | done() 322 | 323 | it 'should preserve try/catch blocks within callbacks', (done) -> 324 | task callbacker11(arg1) 325 | return arg1 326 | caught = [] 327 | try 328 | wait for x from callbacker11 5 329 | fail with 'test1' 330 | catch e 331 | e.should.equal 'test1' 332 | caught.push 1 333 | try 334 | try 335 | wait for x from callbacker11 5 336 | fail with 'test2' 337 | catch e 338 | e.should.equal 'test2' 339 | caught.push 2 340 | fail with 'test3' 341 | catch e 342 | e.should.equal 'test3' 343 | caught.push 3 344 | caught.should.eql [1,2,3] 345 | done() 346 | 347 | it 'should work if the called task takes no arguments', (done) -> 348 | task callbacker4() 349 | return 5 350 | wait for x from callbacker4() 351 | x.should.equal 5 352 | done() 353 | 354 | #gh-52 355 | it 'should support for of loops with callbacks (serial)', (done) -> 356 | task callbacker12(arg1) 357 | return arg1+3 358 | obj = {'this':1,'is':2,'a':3,'test':4} 359 | s = "" 360 | for series p of obj 361 | s += p 362 | wait for x from callbacker12(obj[p]) 363 | s += x 364 | s.should.equal "this4is5a6test7" 365 | done() 366 | 367 | #gh-52 368 | it 'should support for of loops with callbacks (parallel)', (done) -> 369 | task callbacker13(arg1) 370 | return arg1+3 371 | obj1 = {'this':1,'is':2,'a':3,'test':4} 372 | obj2 = {} 373 | t = 0 374 | for parallel p of obj1 375 | t += obj1[p] 376 | wait for x from callbacker13(obj1[p]) 377 | obj2[p] = x 378 | t.should.equal 1+2+3+4 379 | obj2.should.eql {'this':4,'is':5,'a':6,'test':7} 380 | done() 381 | 382 | it 'should accept tail conditionals', (done) -> 383 | task callbacker15 (arg1) 384 | return arg1 * 2 385 | a = 1 386 | wait for x from callbacker15 5 when a isnt 1 387 | wait for x from callbacker15 6 when a is 1 388 | wait for x from callbacker15 x 389 | x.should.equal 6 * 2 * 2 390 | done() 391 | 392 | it 'should support a "safe wait for" syntax for nonstandard callbacks', (done) -> 393 | function nscallbacker1(arg1, next) 394 | next arg1 * 10 395 | task callbacker97(arg1) 396 | return arg1 * 20 397 | a = 3 398 | safe wait for x from nscallbacker1 a 399 | x.should.equal 30 400 | try 401 | wait for x from nscallbacker1 a 402 | catch e 403 | e.should.equal 30 404 | safe wait for x, y from callbacker97(a) 405 | (x exists).should.be.false 406 | y.should.equal 60 407 | done() 408 | 409 | it 'should work with other nested cases', (done) -> 410 | task tester99(ev) 411 | task thing() 412 | return 'text' 413 | try 414 | if ev.match /.*\.txt/ 415 | wait for return_text from thing() 416 | ev.should.equal 'test.txt' 417 | else if ev is 'error' 418 | throw 'ish' 419 | else 420 | # for a non-text file, asynchronously read the file size 421 | wait for file_stats from thing() 422 | return_text = 'File of size '+ file_stats 423 | ev.should.equal 'test.dat' 424 | (return_text in ['text', 'File of size text']).should.be.true 425 | catch e 426 | e.should.equal 'ish' 427 | ev.should.equal 'error' 428 | for series text in ['test.txt','test.dat','error'] 429 | wait for n from tester99 text 430 | done() 431 | 432 | it 'should accept mutliple return values', (done) -> 433 | task boring(b) 434 | return b, b+1, b+2 435 | wait for x, y, z from boring(10) 436 | x.should.equal 10 437 | y.should.equal 11 438 | z.should.equal 12 439 | wait for (x, y, z) from boring(2) 440 | x.should.equal 2 441 | y.should.equal 3 442 | z.should.equal 4 443 | done() 444 | 445 | it 'should work without return values', (done) -> # gh-85 446 | d = 0 447 | task side_effect(a,b,c) 448 | d = a + b + c 449 | function errorizor(next) 450 | next "ERR" 451 | wait for side_effect 1, 2, 3 452 | d.should.equal 6 453 | error_happened = no 454 | try 455 | wait for errorizor() 456 | catch e 457 | error_happened = yes 458 | e.should.equal "ERR" 459 | error_happened.should.be.true 460 | done() 461 | 462 | it 'should support safe no-return syntax', (done) -> #gh-85 463 | function errorizor(next) 464 | next "ERR" 465 | try 466 | safe wait for errorizor() 467 | catch e 468 | print e 469 | require('should').fail 'did not expect an error' 470 | done() 471 | 472 | it 'should preserve "this" across task calls', (done) -> 473 | task this_thing() 474 | pause for 0.001 seconds 475 | return 5 476 | class Tester 477 | task test_thing() 478 | me.x = 1 479 | wait for thing from this_thing() 480 | me.x.should.equal 1 481 | a = new Tester() 482 | wait for a.test_thing() 483 | done() 484 | 485 | -------------------------------------------------------------------------------- /tests/when_expression.kal: -------------------------------------------------------------------------------- 1 | describe 'When Expressions', -> 2 | it 'should allow conditional selection of values using the when, if, except when, and unless keywords', -> 3 | (5 when yes).should.equal 5 4 | require('should').not.exist(5 when no) 5 | (5 if yes).should.equal 5 6 | require('should').not.exist(5 if no) 7 | (5 unless no).should.equal 5 8 | require('should').not.exist(5 unless yes) 9 | (5 except when no).should.equal 5 10 | require('should').not.exist(5 except when yes) 11 | 12 | it 'should allow a false clause using the otherwise or else keywords', -> 13 | (5 when yes otherwise 2).should.equal 5 14 | (5 when no otherwise 2).should.equal 2 15 | (5 when yes else 2).should.equal 5 16 | (5 when no else 2).should.equal 2 17 | (5 if yes otherwise 2).should.equal 5 18 | (5 if no otherwise 2).should.equal 2 19 | (5 if yes else 2).should.equal 5 20 | (5 if no else 2).should.equal 2 21 | (5 unless yes otherwise 2).should.equal 2 22 | (5 unless no otherwise 2).should.equal 5 23 | (5 unless yes else 2).should.equal 2 24 | (5 unless no else 2).should.equal 5 25 | (5 except when yes otherwise 2).should.equal 2 26 | (5 except when no otherwise 2).should.equal 5 27 | (5 except when yes else 2).should.equal 2 28 | (5 except when no else 2).should.equal 5 29 | 30 | it 'should act as an if statement when used in an assignment statement', -> 31 | a = 6 when false 32 | (a exists).should.be.false 33 | a = 6 when true 34 | a.should.equal 6 35 | b = 6 if false 36 | (b exists).should.be.false 37 | b = 6 if true 38 | b.should.equal 6 39 | c = 6 unless false 40 | c.should.equal 6 41 | c = 8 unless true 42 | c.should.equal 6 43 | d = 6 except when false 44 | d.should.equal 6 45 | d = 8 except when true 46 | d.should.equal 6 47 | 48 | 49 | it 'should act as an if statement when used in a return statement', -> 50 | function t1(a) 51 | return 6 when a 52 | return 7 unless a 53 | 54 | function t2(a) 55 | return 8 if a 56 | return 9 except when a 57 | 58 | t1(yes).should.equal 6 59 | t1(no).should.equal 7 60 | t2(yes).should.equal 8 61 | t2(no).should.equal 9 62 | 63 | 64 | it 'should work with no-paren function calls when used as an if statement', -> #Issue 2 65 | a = 'abc' 66 | a = a.concat 's' if false 67 | a.should.equal 'abc' 68 | a = a.concat 'd' if true 69 | a.should.equal 'abcd' 70 | 71 | a = 'abc' 72 | a = a.concat 's' when no 73 | a.should.equal 'abc' 74 | a = a.concat 'd' when yes 75 | a.should.equal 'abcd' 76 | 77 | a = 'abc' 78 | a = a.concat 's' except when true 79 | a.should.equal 'abc' 80 | a = a.concat 'd' except when false 81 | a.should.equal 'abcd' 82 | 83 | a = 'abc' 84 | a = a.concat 's' unless true 85 | a.should.equal 'abc' 86 | a = a.concat 'd' unless false 87 | a.should.equal 'abcd' 88 | 89 | -------------------------------------------------------------------------------- /tests/while_statement.kal: -------------------------------------------------------------------------------- 1 | describe 'While Statement', -> 2 | it 'should loop until the condition is false', -> 3 | i = 1 4 | last_i = 0 5 | while i < 10 6 | i.should.equal last_i + 1 7 | last_i = i 8 | i += 1 9 | i.should.equal 10 10 | 11 | it 'should not execute if the condition is already false', -> 12 | i = 75 13 | while i < 2 14 | require('should').fail('while loop executed when condition was false') 15 | i.should.equal 75 16 | 17 | it 'should define and use the conditional in the appropriate scope', -> 18 | j = 1 19 | x = function () 20 | k = 1 21 | while k < 4 22 | k += 1 23 | while j < 6 24 | j += 1 25 | return k 26 | x().should.equal 4 27 | j.should.equal 6 28 | (k exists).should.equal.false 29 | 30 | it 'should support the "until" syntax', -> #gh-97 31 | i = 1 32 | last_i = 0 33 | until i is 10 34 | i.should.equal last_i + 1 35 | last_i = i 36 | i += 1 37 | i.should.equal 10 38 | --------------------------------------------------------------------------------