├── .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 | [](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 |
--------------------------------------------------------------------------------