UNPKG

sai-language

Version:

An object-oriented language designed to afford code comprehension and maintenance. Transpiles in-place to Javascript.

669 lines (444 loc) 20.5 kB
# This file is automatically generated. _Do not edit._ Unknown Heading ### REFERENCE - _declare global values_ - keywords - ^reference Import/declare global variables at the top of a **.SAI** source file. reference: [name] [expr] ... This is the only way to make global variables; inside a reference declaration. The syntax is the same as a **fields** structure definition, with name/value pairs separated by commas or newlines. reference: MIN 0, MAX 100 express require('express') Vector2D 'Vector2D ^1.0.0' count to MAX debug key .. set @app express() .. set origin to create Vector2D 0, 0 .. Note: you cannot assign to reference variables or re-use them as locals. The following lines would produce compile errors given the above references: local Vector2D set MIN -50 ### OBJECT - _object declaration_ - keywords - ^object Begins the definition of an object. object [identifier] (main) [version] ... Only one object definition is permitted per file. In an object definition, the following sections are supported. See each keyword for more details. Also review the _Defining an Object_ document. reference: [globally defined references] object [identifier] (main) [version] inherit: [list of objects to inherit from] contract: [list of traits that child objects must provide] given: [declaration of immutable object traits] instance: [declaration of initial trait values for each instance] get [trait name to implement dynamically] [code] set given [value] [code] [name] task/process/promise [code that implements this task/process/promise] When a SAI object is created, the **Instantiate** task is executed on that obejct, allowing you to perform instance-level initialization code. Notice the caps; by convention SAI methods are capitalized, while attributes are lowercase. If the **main** clause is included, the object is marked to be instantiated automatically when a compiled `.js` version of the object is required. See **main** for slightly more detail. ### MAIN - _indicates object should be instantiated at runtime_ - keywords - ^main When using the compiler, native Javascript files of objects created with **main** will automatically instantiate a single copy of the object when the file is required. object [objectname] main [objectversion] In other words, when the `.js` file is required, in addition to defining a prototype, an object is also instantiated (which causes any **Instantiate** task to run, thus conceivably launching a program). Main also sets a flag in the **isof** property of the object, indicating it is the main object. Have a look at the `bin` folder for `runner.sai` which uses the __main__ keyword to indicate it is a program that should be run rather than just an object prototype. ### SINGLETON - _singleton object access_ - operators - ^singleton Creates one instance of SAI object by name. .. singleton [expr] [parameters] Creates one instance of SAI object by name. If the object has already been created, returns a reference to it. The scope is that maintained by the runtime library. Like __create__, __singleton__ will attempt to find the object’s source by using the `SAI.config.Loader` function, which defaults to `SAI.GetSourceFromPaths`, which tries to find a file named `[expr].sai` in the provided paths. reference: Tally 'Tally ^1.0.0' .. set inventory to singleton Tally (The example follows best practices of placing object names in a **reference** section, aliasing versioned names into literals.) Singleton objects are useful for library functions, APIs to external resources, tallies, caches, message managers and so on. Since the reference to the singleton object is maintained by the library, singleton objects are never garbage collected (and cannot be destroyed) once created. If you might need to release resources held by a singleton before the end of the application, you must make allowances for that in your object's API; e.g. by crafting a `.Cleanup` task. See also __new__, which is a direct link to Javascript's `new`, and __create__, which will make a new object each time. ### CONTRACT - _requirements for child objects_ - keywords - ^contract When defining an object, **contract** is used to specify tasks or traits that _children_ inheriting from this object are intended to implement. contract: [task/trait name] [task/trait name] ... Inheriting from an object that has contracts, and then not providing implementations for those contracts, will result in a SAI exception. object fruit contract: Consume object apple inherit: fruit Consume task debug 'You ate an apple.' object pear inherit: fruit > exception thrown: “SAI: Contractually required task ‘Consume’ does not exist in object ‘pear’.” ### GIVEN - _parameter declaration and static values_ - keywords - ^given Use __given__ to name the parameters to a function, and to define immutable traits. .. given p1, p2, p3 ... - or - [ object declaration ] given [definition of values] To declare parameters: set [ident] (given [$ var]) // dynamic trait .. task (given [parameter1], [parameter2], ...) .. process (given [parameter1], [parameter2], ...) .. promise (given [parameter1], [parameter2], ...) An example of parameter naming: set tally to blank set AddRow to task given item, quantity set tally[item] to (self default 0) + quantity AddRow 'socks', 3 AddRow 'pants', 6 debug tally > { socks: 3, pants: 6 } #### given object constants Defines immutable traits when declaring an object. These traits are assigned to the object prototype itself and locked/frozen; they cannot be changed and yet are available in every instance of an object. Givens are useful for static data. object Apple instance: varietal 'unknown' given: species 'M. domestica' Describe task return 'undefined var undefined .. set apple to create 'Apple' set apple.varietal to 'Macintosh' debug !apple.Describe > M. domestica var Macintosh **Given** traits can only be changed/overridden through inheritance. object Crabapple inherit: Apple given: species 'M. coronaria' .. set specimen to create 'Crabapple' debug !specimen.Describe > 'M. coronaria var unknown' ### INSTANCE - _object instance variable declaration_ - keywords - ^instance Define initial values for an object’s traits. instance: [varname] [initial value] ... When an object is created, instance traits are assigned the given values before the object’s **Instantiate** task is called. object Sock 1.0.0 instance: colour 'Brown' pattern 'Argyle' size 'M' kind 'dress' Instantiate task debug @ select list colour, pattern, size, kind .... set aSock to create 'Sock' > { colour: 'Brown', pattern: 'Argyle', size: 'M', kind: 'dress' } Instance traits, when declared with __instance__ or __given__, do not need to have their scope indicated with `@` when used in their own object. Said another way, declared instance variables are automatically scoped to the object. This is similar to the way that __reference__ values don't need to use the `~` global scoping prefix. The following are equivalent: instance: x 0 y 0 ... set x 34 set @x 34 // this is not necessary When referring to another object's instance variables, you will need to properly scope them: CopyPoint given point set x to point.x set y to point.y Instance variables cannot be initialized with functions or "complex" expressions. The compiler will warn you about it. Nevertheless, you should still declare all instance variables, even if you must start them with `undefined`. That's because the compiler looks at the instance variables you've declared to know what your instance variables are. object AppleCrate instance: sku '448893003' label ~GetLabel(.sku) /// this won't work, instead: instance: sku '448893003' label undefined Instantiate task set label ~GetLabel(.sku) ### GET - _dynamic object attribute_ - keywords - ^get Declares a _getter_ for a dynamic object trait; that is, a trait that has a value that is only calculated upon request. object [name] [version] [ident] get [code] return [value] ( set ( given [ident] ) [code] A brief example: object Vector2 1.0.0 magnitude get // dynamic getter for 'magnitude' trait return Math.sqrt(x*x + y*y) set given m // dynamic setter for 'magnitude' trait set a to angle set x to m * Math.cos(a) set y to m * Math.sin(a) You can have a getter without a setter, and a setter without a getter. documentation get return ''' Lots of information about this object. tally set given token inc _tally oken ### | SET - _replacement operator_ - pipers - ^set A chainable comprehension operator that allows direct reference and replacement of the incoming dataset within an expression or code block, using the **it** pronoun. ... | set [expr] ... | set (AS var1, var2...) [indent code block] ... | set USING [function reference] **Set** can be used with an expression: debug 4 | set 5*it+2 | set it/7 > 3.142857142857143 **Set** can use an indented code block: debug friends by .age | set set .length to 3 > [ { name: 'Doug', age: 18, province: 'ON' }, > { name: 'John', age: 19, cat: true, dog: true, province: 'ON' }, > { name: 'Marshal', age: 21, dog: true, province: 'ON' } ] _If you don’t specifically **return** a value or object from within an **set** code block, the original value will be used (as in the example above). In other words, there is an implicit `return it` at the end of every **set** block._ **Set** supports the **using** clause, in which case the function specified receives the original value as its first parameter, and the return value is passed forward. The two debug statements below are equivalent set ExtractFirst to task return $[0] debug friends #cat | set Extract(it) debug friends #cat | set using Extract > { name: 'Sara', age: 23, cat: true, province: 'ON' } > { name: 'Sara', age: 23, cat: true, province: 'ON' } You must specifically return a value in the function called by **set using**. ### TASK - _define a function_ - keywords - ^task Define a block of code as an object trait or anonymous function. [identifier] task ( given [parameters] ) / ( expects [parameters] ) [code] .. [task] ( given [parameters] ) / ( expects [parameters] ) [code] It’s probably best to explain this by showing what Javascript is made. #### task trait The first example is a task trait on an object. It takes two standard parameters, name and value. Instrument task given name, value debug `${@context} ${name}: ${value} Generated code: function (p, $value) { var $name = p, $ = this; console.log('' + $.context + ' ' + $name + ': ' + $value); } You’ll note that the first parameter in a SAI-generated function is always `p`, which is explained below. More importantly, however, note the assignation `$ = this`. In SAI-generated code, the `$` variable is used as a buffer for the _this_ context. When executing a task trait, SAI captures the current _this_ with `$=this` to provide a reliable context for anonymous functions. #### anonymous task Here’s the same task but for anonymous use: set anon to task given name, value debug `${@context} ${name}: ${value} Generated code: function (p, $value) { var $name = p; console.log('' + $.context + ' ' + $name + ': ' + $value); } Notice how the anonymous task doesn’t capture `this.` into the `$` variable the way the trait task does. Thus, anonymous tasks automatically bind to the context that created them. This is usually what you want when creating anonymous tasks, and is why the `var self=this` idiom has so much traction in Javascript. SAI builds this idiom into the language. However, if you don’t want this behaviour for a particular anonymous task, include the **orphan** statement in it. set anon to task given name, value orphan debug `${@context} ${name}: ${value} Generated code: function (p, $value) { var $name = p; var $ = this; console.log('' + $.context + ' ' + $name + ': ' + $value); } Orphan anonymous tasks bind to the _calling_ context, rather than to the context that created them. #### positional vs. named parameters The second example is a task which expects two named parameters, $angle and $magnitude. As shown above, the first parameter in generated code is always `p`. Named parameters are passed as traits within `p`. SetPolar task expects $angle, $magnitude set x to $magnitude * Math.cos($angle) set y to $magnitude * Math.sin($angle) Generated code: function (p) { var $ = this; _$AI.expectsThrow(p, { "angle": true, "magnitude": true }, 'SetPolar'); $.x = (p.magnitude * Math.cos(p.angle)); $.y = (p.magnitude * Math.sin(p.angle)); } When you use **expects**, a call to _expectsThrow_ is included to validate your assumptions. This task is called by naming the parameters: SetPolar angle 45o, magnitude 3 Which generates this code: $.SetPolar({ angle: 0.7853981633974483, magnitude: 3 }); Notice how named parameters are encapsulated in a plain JS object and passed in the first function call argument (which is always named `p`). It is not necessary to use the **expects** clause to used named parameters. All **expects** does for you is check to see if the names (and types) are as expected. Here is the function without it: SetPolar task set x to $magnitude * Math.cos($angle) set y to $magnitude * Math.sin($angle) Generating this: function (p) { var $ = this; $.x = (p.magnitude * Math.cos(p.angle)); $.y = (p.magnitude * Math.sin(p.angle)); } The use of named parameters with the **$** scoping prefix generates code that assumes the first argument `p` will have the expected named traits. Refer to the entry on **expects** for more on what it does. ### PROCESS - _generative function definition_ - keywords - ^process Creates a function that is expected to **yield** one or more values. [identifier] process ( as [parameter list] ) [yielding code block] .. process ( as [parameter list] ) [yielding code block] This is an ES6 feature. **Process** does exactly the same thing as **task** except the generated code uses `function*` instead of `function`. If you're not familiar with generators/yielding, here is a very simplified overview. set Candidates to process count 1 to 100 as x count 1 to 100 as y if 1<x and x<y and x+y<100 yield: x x, y y, s x+y, p x*y The Candidates process, when invoked, runs all of the code in the process up until the first **yield**, then returns, handing you a process object, here stored in the variable `iter`: set iter Candidates() That object is an _iterator_; it is a stateful representation of a `Candidates` process. Each time you call a process, you get a new iterator. Once you have an iterator, you can _iterate_ through it. Each iteration returns the next value _yielded_ by the process. Iteration takes place by calling the **.next** task on the iterator. debug iter.next() > { value: { x: 2, y: 3, s: 5, p: 6 }, done: false } You can see that **.next** returns a simple object with two fields, **.value** and **.done**. (In our iterator, the value is the first qualified candidate represented by a simple object with four fields: x, y, s and p.) **.value** holds whatever was **yield**ed. **.done** is a boolean flag that indicates whether the iteration is complete; that is, whether another call to **.next** would result in a newly generated value. Each time you call **.next** you will get a new value. debug iter.next() debug iter.next() debug iter.next() > { value: { x: 2, y: 4, s: 6, p: 8 }, done: false } > { value: { x: 2, y: 5, s: 7, p: 10 }, done: false } > { value: { x: 2, y: 6, s: 8, p: 12 }, done: false } Here is a bit of code that will keep calling **.next** until the **.done** flag is set, saving the result in a four item list. set cache to empty // empty array dountil result.done // check .done at the end of each loop set result iter.next() // next iteration to result cache.push result // save result in cache debug cache | limit -4 // print last four rows > [ { value: { x: 48, y: 50, s: 98, p: 2400 }, done: false }, { value: { x: 48, y: 51, s: 99, p: 2448 }, done: false }, { value: { x: 49, y: 50, s: 99, p: 2450 }, done: false }, { value: undefined, done: true } ] Once the **.done** flag is set, the iterator is said to be _exhausted_. Notice that **.value** is *undefined* when the **.done** flag is set. Processes, generators and iterators are very powerful tools because they allow computations to be *lazy*, that is, for data to be generated only as it is needed, thus vastly reducing memory requirements. (The Python language is very useful for scientific computing because it is a primarily lazy language because large data sets can be manipulated with modest resources.) SAI has many affordances for processes and iterators, including native support with all comprehensions. As a very simple example, all of the above code could be replaced with this simple expression: debug Candidates() | limit -4 > [ { x: 48, y: 49, s: 97, p: 2352 }, { x: 48, y: 50, s: 98, p: 2400 }, { x: 48, y: 51, s: 99, p: 2448 }, { x: 49, y: 50, s: 99, p: 2450 } ] (Notice how the limit comprehension itself deals silently with **.next** and **.done**, and automatically gives you just a list with only the **.value** of each iteration.) ### PROMISE - _declare a Promise-producing function_ - keywords - ^promise Wraps the code block in a Promise-like function shell. object [identifier] promise ( as [parameter list] ) [code block] .. promise ( as [parameter list] ) [code block] Along with **resolve** and **reject**, forms a convenient bit of syntactic sugar for making Promise-like functionality. Here is some sample code: set willIGetNewPhone promise given isMomHappy if isMomHappy resolve: brand 'Wangdoodle' colour 'paisley' else reject new ~Error 'Mom is not happy.' showOff promise given phone with phone debug 'Hey friend, I have a new ${.colour} ${.brand} phone' askMom task given happiness chain willIGetNewPhone(happiness) then showOff catch promise given e debug '${e} -- No phone for you.' askMom true askMom false > Hey friend, I have a new paisley Wangdoodle phone > Error: Mom is not happy. -- No phone for you. The way **promise** works is best explained by just showing you what happens when you use it. We wrap the code block in a Promise constructor, as follows: set doThing to promise given url HeyServer url, task given request, result if result.success resolve result else reject result .. debug doThing.toString() > function (p) { > return new Promise(function($_resolve, $_reject) { > var $url = p; > $HeyServer($url, function(p, $result) { > var $request = p; > if (($56 = ($result.success))) { > $_resolve($result); > } else { > $_reject($result); > } > }); > }); > } So basically it throws a **Promise** wrapper around the function and allows you to use **resolve** and **reject** as statements. (The $56 in there is housekeeping for the **trial** pronoun; V8 optimizes unused assignments like this away.)