GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing beyondloom.com — Lil: A Scripting Language Lil: A Scripting LanguageLil is part of the technology that powers Decker, a multimedia creative tool inspired by HyperCard. Decker uses Lil for adding custom behavior to decks and the widgets within. Lil is designed to be learned in layers, but it is a richly multi-paradigm language which incorporates ideas from imperative, functional, declarative, and vector-oriented languages. on mode a do # line comment r:() each x in a r[x]:1+r[x] end first extract key orderby value desc from r end mode[1,2,2,3,4,2,1] ![]() Lil's implementation needs to be fairly small, as an interpreter is shipped with every standalone Decker document along with the rest of the runtime. At the same time, Lil should be prepared for the everyday needs of Decker users, with primitives to handle common use-cases and a minimum of boilerplate. The language therefore tries to thread the needle between a design which is simple and a design which is ergonomically pleasant. There are a small number of datatypes, features and primitives which compose in many useful ways. The query syntax generalizes to manipulation of dictionaries, lists, and strings, and replaces many individual primitive operations in other languages with a single algorithmic framework. Simple things are easy, and complex things are possible.
Lil, A PrimerLet's begin by looking at a few practical examples of Lil scripts you might encounter or compose while using Decker, to give you a loose impression of the language. Consider the following script on a button widget: on click do # adventure awaits! go["DungeonEntrance" "BoxIn" 15] end The In another button script, clicking the button is only meant to navigate to another card if the user has previously found a "key", represented as a checkbox widget named on click do if inventory.widgets.haskey.value play["DoorSqueak"] go["TheCrypt"] else alert["The door is locked!"] end end The For more elaborate conditions you can form expressions using on click do i:inventory.widgets goldCoins:i.money hasPass:i.trollPass.value if hasPass & (goldCoins.value>5) alert["The troll charges you 5 gold coins."] goldCoins.value:goldCoins.value-1 go["TrollTavern"] else alert["The troll gruffly refuses you entry."] end end The colon If you're looking to add simple interactive behaviors to your decks, you may already know enough to get started. The rest of this reference manual will focus on Lil itself, separately from its usage within Decker. You can find more practical examples and scripting tutorials among the Types and ConversionsThere are 8 types of value in Lil: numbers, strings, lists, dictionaries, tables, functions, interfaces, and the nil value.
Lil has a soft, spongy, dynamic type system in which values do their best to convert to a more relevant type as the need arises.
When indexing into values, we will uniformly refer to elements:
The number zero ( Lil, the Imperative LanguageLil scripts are a sequence of expressions, including literal values, references or assignments to variables, conditionals ( Variable and function names may contain any alphanumeric characters (as well as a: 23
longer_name: "a string"If a value has never been explicitly placed in a variable, it contains nil. You can access or assign to elements of a list or dictionary by subscripting with brackets: (11,22,33)[1] "String"[2] l[4]:2 d["fruit"]:"apple" For convenience, indexing may also be performed with "dot notation", where a name after the dot is treated as a string index. These expressions are equivalent: d["fruit"]:"cherry" d.fruit:"cherry" If you index a list with integers within range of its b: 5 # b contains the number 5 b[0]: 5 # b becomes a length-1 list containing the number 5 c # c contains nil c.fruit: "yes" # c now contains a dictionary with the key "fruit" and the value "yes" When the leftmost part of an assignment expression is an expression- not just a variable name- the value of that expression is amended and a new value with appropriate changes is returned: (11,22,33)[1]:44 # produces the list (11,44,33) "Cat"[1]:"ive" # produces the string "Civet" ().baz:99 # amend the empty list at index "baz" (promoting it to a dictionary) with the value 99, # producing the single-element dictionary {"baz":99} (11,22).baz:33 # extend (11,22) into a dictionary, and bind "baz", giving {0:11,1:22,"baz":33} Note the important distinctions between these similar-looking cases: foo:11,22,33 foo[1]:44 # get the value in "foo", amend it at index 1, and store the amended list in "foo" again. (foo)[1]:44 # the parenthesized portion is a subexpression which gets the value in "foo". # amend it at index 1, and return the amended list, leaving "foo" unchanged. (foo[1]):44 # this is not syntactically valid! # assignment to a subexpression must have a dotted or bracketed index. If you index with a dot followed immediately by another dot or open-bracket it means "each element". Use this for drilling into nested lists or dictionaries: t:("AB","CD","EFG") t[1] # "CD" # the 1st element of t t.[1] # ("B","D","F") # the 1st element of each element of t d.a.key:"apple" d.b.key:"pear" d # {"a":{"key":"apple"},"b":{"key":"pear"}} d.a # {"key":"apple"} d.a.key # "apple" d..key # {"a":"apple","b":"pear"} Lil has a number of unary and binary operators- see appendix 1 and 2 for details. Many, like 3*2+5 # 21 3*(2+5) # 21 (3*2)+5 # 11 You can make decisions with if 1>2 "narp" # this doesn't happen end if 5 "yarp" # 5 is truthy, so this does happen! end if 5 "also yarp" # this happens... else "also narp" # but this does not! end if 1>2 "narp" # this doesn't happen elseif 1<2 "yarp" # but this does, else "narp" # so this doesn't end You can iterate over the elements of a value with an each x in 3,5,7 x end each val key in d val,key end r:0 each x in range 10 r:r+x end You can also repeat a body of code an indefinite number of times with a a:5 while a>3 print[a] # prints 5, then 4, then stops. a:a-1 end To declare a function, use the keyword on pair x y do x,y end pair[3 5] # returns a list of 3 and 5 pair[3] # returns a list of 3 and nil pair[3 5 7] # returns a list of 3 and 5 pair[3,5] # careful: ',' joins items into lists- it is not an argument separator! pair 3 5 # wrong: this is three separate statements, not a function call! If you want to define a function that takes a variable number of arguments- a so-called variadic or ambivalent function- declare the function with a single named argument prefaced with on several ...x do 1-x end several[11 22 33] # (-10,-21,-32) Lil, the Functional LanguageAs we've seen above, thinking about Lil as an everyday garden-variety imperative language is perfectly sufficient for writing scripts. Some characteristics, though, make it well-suited to the functional style of programming, in which we aim to minimize mutation, compose our program from functions which do not have "side-effects", and make use of so-called "higher-order" functions- functions which take functions as arguments. Values in Lil are truly values. They have copy-on-write semantics: an assignment to part of a value creates an entirely new value, leaving any other references to the original value unchanged: a:1,2,3 b:a b[1]:5 show[a] # 1,2,3 show[b] # 1,5,3 Statements in Lil are always expressions. That is, they always return a value and can be composed within larger expressions. Assignments evaluate to the value being assigned. x:y:z # assign both x and y the value of z a: if x>5 99 else 33 end # assign a to either 99 or 33 b:1 c: while b<100 b:b*2 end # assign c to 128 The each x in 3,5,7 # (300,500,700) x*100 end t.foo:"one" t.bar:"three" each v in t # {"foo":3,"bar":5} count v end You can pass functions to other functions by name: on apply func do func["two"] end on twice x do x,x end apply[twice] # ("two","two") Since apply[on thrice x do x,x,x end] One advantage of functions always having a bound name is that there's no special combinator plumbing required for anonymous recursion. If the name never matters, consider using the name Lil uses lexical scope: variables will resolve to the closest nested binding available, and the local variables of a caller to a function will not be visible or modified by the callee (unless the callee's definition is nested in the caller): global:333 on quux x do v:99 x[77] print[v] # 99 end on zami x do v:23 print[global,v,x] # 333,23,77 end quux[zami] Furthermore, functions close over variables in their lexical scope, allowing for encapsulated "objects" with their own mutable state: on counter x do on inc do x:x+1 end end a:counter[100] b:counter[200] print[a[]] # 101 print[a[]] # 102 print[b[]] # 201 print[a[]] # 103 show[list x] # (nil) Whenever an assignment is carried out, if the variable name in question has been assigned to in any surrounding lexical scope, the assignment will update the closest definition. If the name has never been assigned to before, a new local variable will be created at the current scope. The It is also possible to explicitly define a local variable by using the keyword duplicate:"Alpha" on func do local duplicate:"Beta" show[duplicate] # "Beta" end func[] show[duplicate] # "Alpha" Finally, if you're mentally wed to the idea of expressing algorithms recursively, you may be pleased to discover that Lil supports tail-call elimination. If a recursive function calls itself (or another function) as the final operation in the function, it will not consume extra stack space: on addrec x y do if x>0 1+addrec[x-1 y] # addition happens after call returns. not tail-recursive! else y end end on addtail x y do if x>0 addtail[x-1 y+1] # tail-recursive call. else y end end addtail[80000 5] # this won't blow the stack. Lil, the Query LanguageSo far, we haven't looked much at tables. While one can operate on tables by indexing them or iterate over their contents with an A query consists of an operation, a sequence of column expressions (possibly with names), any number of
If a query has multiple clauses, they're carried out right-to-left, just like Lil expressions. You can think of each clause as taking a table and a column expression, evaluating the expression within the context of the table (binding each table column as a variable), and using the resulting column to transform the input table into a different table. The clauses are:
When column expressions are evaluated, several "magic" columns and variables are automatically defined:
When "ungrouping" or collecting the final result columns, the table is rectangularized: each group will have as many result rows as the column of maximum Given a simple table: people.name:"Alice","Sam","Thomas","Sara","Walter" people.age:25,28,40,34,43 people.job:"Developer","Sales","Developer","Developer","Accounting" people:table people Let's perform some selections: select from people # select all columns, like "select *" in SQL # +----------+-----+--------------+ # | name | age | job | # +----------+-----+--------------+ # | "Alice" | 25 | "Developer" | # | "Sam" | 28 | "Sales" | # | "Thomas" | 40 | "Developer" | # | "Sara" | 34 | "Developer" | # | "Walter" | 43 | "Accounting" | # +----------+-----+--------------+ select name from people # select a specific column # +----------+ # | name | # +----------+ # | "Alice" | # | "Sam" | # | "Thomas" | # | "Sara" | # | "Walter" | # +----------+ select firstName:name dogYears:7*age from people # compute and rename columns # +-----------+----------+ # | firstName | dogYears | # +-----------+----------+ # | "Alice" | 175 | # | "Sam" | 196 | # | "Thomas" | 280 | # | "Sara" | 238 | # | "Walter" | 301 | # +-----------+----------+ select name where name like "S*" from people # where takes a boolean (0/1) column # +--------+ # | name | # +--------+ # | "Sam" | # | "Sara" | # +--------+ select name index orderby name asc from people # orderby takes a column of values and "asc" or "desc" # +----------+-------+ # | name | index | # +----------+-------+ # | "Alice" | 0 | # | "Sam" | 1 | # | "Sara" | 3 | # | "Thomas" | 2 | # | "Walter" | 4 | # +----------+-------+ select name job by job orderby name asc from people # sort names within each group # +----------+--------------+ # | name | job | # +----------+--------------+ # | "Alice" | "Developer" | # | "Sara" | "Developer" | # | "Thomas" | "Developer" | # | "Sam" | "Sales" | # | "Walter" | "Accounting" | # +----------+--------------+ select name job orderby (job join name) asc from people # sort by multiple columns # +----------+--------------+ # | name | job | # +----------+--------------+ # | "Walter" | "Accounting" | # | "Alice" | "Developer" | # | "Sara" | "Developer" | # | "Thomas" | "Developer" | # | "Sam" | "Sales" | # +----------+--------------+ All result columns will be repeated to match the select employed:(count name) job by job from people # +----------+--------------+ # | employed | job | # +----------+--------------+ # | 3 | "Developer" | # | 3 | "Developer" | # | 3 | "Developer" | # | 1 | "Sales" | # | 1 | "Accounting" | # +----------+--------------+ select employed:(count name) job:(first job) by job from people # +----------+--------------+ # | employed | job | # +----------+--------------+ # | 3 | "Developer" | # | 1 | "Sales" | # | 1 | "Accounting" | # +----------+--------------+ When computing columns, you're working with lists of elements, and taking advantage of the fact that primitives like on avg x do (sum x) / count x end select job:(first job) avg_age:avg[age] by job from people # +--------------+---------+ # | job | avg_age | # +--------------+---------+ # | "Developer" | 33 | # | "Sales" | 28 | # | "Accounting" | 43 | # +--------------+---------+ The same applies to any functions called in a The update job:"Engineer" where job="Developer" from people # +----------+-----+--------------+ # | name | age | job | # +----------+-----+--------------+ # | "Alice" | 35 | "Engineer" | # | "Sam" | 28 | "Sales" | # | "Thomas" | 50 | "Engineer" | # | "Sara" | 44 | "Engineer" | # | "Walter" | 43 | "Accounting" | # +----------+-----+--------------+ As with update manager:random[name] where job="Developer" from people # +----------+-----+--------------+---------+ # | name | age | job | manager | # +----------+-----+--------------+---------+ # | "Alice" | 25 | "Developer" | "Sara" | # | "Sam" | 28 | "Sales" | nil | # | "Thomas" | 40 | "Developer" | "Sara" | # | "Sara" | 34 | "Developer" | "Sara" | # | "Walter" | 43 | "Accounting" | nil | # +----------+-----+--------------+---------+ The t:update manager:random[name] where job="Developer" from people select name "manager":"Julia" fill manager from t # +----------+----------+ # | name | manager | # +----------+----------+ # | "Alice" | "Thomas" | # | "Sam" | "Julia" | # | "Thomas" | "Thomas" | # | "Sara" | "Thomas" | # | "Walter" | "Julia" | # +----------+----------+ "n/a" fill t # +----------+-----+--------------+----------+ # | name | age | job | manager | # +----------+-----+--------------+----------+ # | "Alice" | 25 | "Developer" | "Thomas" | # | "Sam" | 28 | "Sales" | "n/a" | # | "Thomas" | 40 | "Developer" | "Thomas" | # | "Sara" | 34 | "Developer" | "Thomas" | # | "Walter" | 43 | "Accounting" | "n/a" | # +----------+-----+--------------+----------+ When mixing queries with other types of code, it may be very useful to execute a query and get back simple lists, strings, or numbers. The jobs:extract first job by job from people # ("Developer","Sales","Accounting")
extract value orderby value asc from jobs # sort a list # ("Accounting","Developer","Sales") extract index orderby value asc from jobs # grade a list # (2,0,1) extract value orderby index desc from jobs # reverse a list (see 'rev') # ("Accounting","Sales","Developer") extract list index by value from "ABBAAC" # group a list # ((0,3,4),(1,2),(5)) extract list value by floor index/3 from "ABCDEFGHI" # partition a list (see 'window') # (("A","B","C"),("D","E","F"),("G","H","I")) extract first value by value from "ABBAAC" # unique items in a list (see 'distinct') # ("A","B","C") If names are specified, all results are collected into a dictionary: extract a:first age b:last age orderby age asc from people # {"a":(25),"b":(43)} If no columns are specified, the result will be the first column of the table: extract orderby value asc from "BEDAC" # ("A","B","C","D","E") The insert name job age with "John" "Writer" 32 end # +--------+----------+-----+ # | name | job | age | # +--------+----------+-----+ # | "John" | "Writer" | 32 | # +--------+----------+-----+ insert name job age with "John" "Writer" 32 into people # +----------+-----+--------------+ # | name | age | job | # +----------+-----+--------------+ # | "Alice" | 25 | "Developer" | # | "Sam" | 28 | "Sales" | # | "Thomas" | 40 | "Developer" | # | "Sara" | 34 | "Developer" | # | "Walter" | 43 | "Accounting" | # | "John" | 32 | "Writer" | # +----------+-----+--------------+ As a special case, if the value to insert into is a number, treat it as an empty table; Lil also offers two basic joining operations: jobs:insert job salary with "Sales" 85000 "Developer" 75000 "Accounting" 60000 "Facilities" 50000 end # +--------------+--------+ # | job | salary | # +--------------+--------+ # | "Sales" | 85000 | # | "Developer" | 75000 | # | "Accounting" | 60000 | # | "Facilities" | 50000 | # +--------------+--------+ people join jobs # +----------+-----+--------------+--------+ # | name | age | job | salary | # +----------+-----+--------------+--------+ # | "Alice" | 25 | "Developer" | 75000 | # | "Sam" | 28 | "Sales" | 85000 | # | "Thomas" | 40 | "Developer" | 75000 | # | "Sara" | 34 | "Developer" | 75000 | # | "Walter" | 43 | "Accounting" | 60000 | # +----------+-----+--------------+--------+ guests:insert name with "Alice" "Joan" "Oscar" "Thomas" end # +----------+ # | name | # +----------+ # | "Alice" | # | "Joan" | # | "Oscar" | # | "Thomas" | # +----------+ select a:name b:name_ where name < name_ from guests cross guests # +---------+----------+ # | a | b | # +---------+----------+ # | "Alice" | "Joan" | # | "Alice" | "Oscar" | # | "Joan" | "Oscar" | # | "Alice" | "Thomas" | # | "Joan" | "Thomas" | # | "Oscar" | "Thomas" | # +---------+----------+ For a row-wise join- concatenating tables with the same columns- you can use "name" take people # +----------+ # | name | # +----------+ # | "Alice" | # | "Sam" | # | "Thomas" | # | "Sara" | # | "Walter" | # +----------+ ("age","job") take people # +-----+--------------+ # | age | job | # +-----+--------------+ # | 25 | "Developer" | # | 28 | "Sales" | # | 40 | "Developer" | # | 34 | "Developer" | # | 43 | "Accounting" | # +-----+--------------+ 3 drop people # +----------+-----+--------------+ # | name | age | job | # +----------+-----+--------------+ # | "Sara" | 34 | "Developer" | # | "Walter" | 43 | "Accounting" | # +----------+-----+--------------+ (0,2,3) take people # +----------+-----+-------------+ # | name | age | job | # +----------+-----+-------------+ # | "Alice" | 25 | "Developer" | # | "Thomas" | 40 | "Developer" | # | "Sara" | 34 | "Developer" | # +----------+-----+-------------+ The expenses:insert kind jan feb with "tax" 11 55 "gas" 22 66 "power" 33 77 "food" 44 88 end # +---------+-----+-----+ # | kind | jan | feb | # +---------+-----+-----+ # | "tax" | 11 | 55 | # | "gas" | 22 | 66 | # | "power" | 33 | 77 | # | "food" | 44 | 88 | # +---------+-----+-----+ flip expenses # +-------+-----+-----+-------+------+ # | key | tax | gas | power | food | # +-------+-----+-----+-------+------+ # | "jan" | 11 | 22 | 33 | 44 | # | "feb" | 55 | 66 | 77 | 88 | # +-------+-----+-----+-------+------+ The "key" drop flip expenses # +-----+-----+-------+------+ # | tax | gas | power | food | # +-----+-----+-------+------+ # | 11 | 22 | 33 | 44 | # | 55 | 66 | 77 | 88 | # +-----+-----+-------+------+ In some situations you may wish to construct or query tables containing columns which have names that are reserved keywords (like denormal: select "with \"escapes":index "count":value from "ABC" # +---------------+-------+ # | with "escapes | count | # +---------------+-------+ # | 0 | "A" | # | 1 | "B" | # | 2 | "C" | # +---------------+-------+ insert "pet name" "pet species" with "Galena" "Chicken" "Pippi" "Chicken" "Chester" "Toad" end # +-----------+-------------+ # | pet name | pet species | # +-----------+-------------+ # | "Galena" | "Chicken" | # | "Pippi" | "Chicken" | # | "Chester" | "Toad" | # +-----------+-------------+ To reference these columns in a query, every column expression has the variable select where column["with \"escapes"]>0 from denormal # +---------------+-------+ # | with "escapes | count | # +---------------+-------+ # | 1 | "B" | # | 2 | "C" | # +---------------+-------+ first extract column from denormal # +---------------+-------+-------+--------+-------+ # | with "escapes | count | index | gindex | group | # +---------------+-------+-------+--------+-------+ # | 0 | "A" | 0 | 0 | 0 | # | 1 | "B" | 1 | 1 | 0 | # | 2 | "C" | 2 | 2 | 0 | # +---------------+-------+-------+--------+-------+ Extracting In general, use It may also be helpful to think in terms of how the shape of output tables relates to input tables for various table operations:
Lil, the Formatting LanguageThe f: "0x%04h" # a four-digit zero-padded lowercase hexadecimal number with an '0x' prefix f format 123 # "0x007b" f parse "0x007b" # 123 Format strings consist of a sequence of patterns and literals. Patterns always begin with a
Pattern types are as follows:
When parsing, each pattern will be matched against input in sequence, consuming some number of input characters and producing output values, and literals will be expected; if at any point a pattern or literal fails to match against input, parsing will cease, and any subsequent patterns in the format string will yield nil. Thus, a given format string will always yield a fixed number of results, no matter the input. In the case where there is exactly one value-yielding pattern in the format string, "%f %s %i" parse "12 apples" # (12,"apples",nil) "%f %ss" parse "12 apples" # (12,"apple") ("amount","noun") dict "%f %ss" parse "12 apples" # {"amount":12,"noun":"apple"} "[%s]" parse "[something]" # "something" The right argument to form: "%6s%6c%2i" data: "apple $1.00 1\ncherry $0.3515\nbanana $0.75 2" r: form parse "\n" split data #(("apple ",1,1),("cherry",0.35,15),("banana",0.75,2)) table r #+----------+------+----+ #| c0 | c1 | c2 | #+----------+------+----+ #| "apple " | 1 | 1 | #| "cherry" | 0.35 | 15 | #| "banana" | 0.75 | 2 | #+----------+------+----+ flip r #(("apple ","cherry","banana"),(1,0.35,0.75),(1,15,2)) t: table ("name","price","amt") dict flip r #+----------+-------+-----+ #| name | price | amt | #+----------+-------+-----+ #| "apple " | 1 | 1 | #| "cherry" | 0.35 | 15 | #| "banana" | 0.75 | 2 | #+----------+-------+-----+ ("\n",form) format t # round-trip #"apple $1.00 1\ncherry $0.3515\nbanana $0.75 2" When formatting, literals will be included in the output string and each pattern will control conversion of one value from the input list, with a few exceptions as explained below. Missing arguments will be interpreted as nil. "%i,%a,%i" format 1,(list 65,66,67) # "1,ABC,0" The left argument to () format 11,22 # (11,22) # (identity) "%03i" format 11,22 # "011" # format the first item and discard unused(!) (list "%03i") format 11,22 # ("011","022") # format each (":","%03i") format 11,22 # "011:022" # fuse (format each) ("<%s>",":","%03i") format (list 11,22),(list 33) # ("<011:022>","<033>") # format each (fuse (format each)) ("@","<%s>",":","%03i") format (list 11,22),(list 33) # "<011:022>@<033>" # fuse (format each (fuse (format each))) Recursive formats will "explode" tables into lists of row-lists: t: insert alpha beta with "one" 11 "two" 22 end #+-------+------+ #| alpha | beta | #+-------+------+ #| "one" | 11 | #| "two" | 22 | #+-------+------+ (list "%u - %i") format t #("ONE - 11","TWO - 22") If The "%p" parse "2021-02-03T04:05:58Z" # {"year":2021,"month":2,"day":3,"hour":4,"minute":5,"second":58} "%p" format ().year:1984 # "1984-00-00T00:00:00Z" The data: "one,two,three" "%s,%n" parse data # ("one",4) "%s,%n" parse 4 drop data # ("two",4) "%*sA%n" parse "BBCABA" # the index of the first 'A' is 4 The "%i%m" parse "23" # (23,1) # successful parse "%i%m" parse "0" # (0,1) # successful parse of 0 "%i%m" parse "orange" # (nil,0) # mismatch The "%.2r01" parse "01110201" # "01110" # grab any leading number of binary digits "%*.2r01%z" parse "010","012" # (1,0) # string consists only of binary digits? "%r-%i" parse "----45" # ("----",45) # grab any prefix of minus signs "%o-" parse "A","-A","--A" # ("","-","-") # grab at most one minus sign "%*o-%i" parse "-45" # 45 # discard any sign, and read an unsigned int "#%-r\n\n%s" parse "# comment\nA" # (" comment","A") # read a Lil-style line comment until a newline The "%v[%q]%m" parse "func[\"foo\"]" # ("func","foo",1) "%q" format "a string" # "\"a string\"" The "%j" format (11,22) dict (33,44) # "{\"11\":33,\"22\":44}" "%j" format list 11,22 # "[11,22]" "%j" format table 11,22 # "[{\"value\":11},{\"value\":22}]" "%j" format on x do 2+x end # "null" When parsing JSON, the value "%j" parse "[true,false,null,1]" # (1,0,nil,1) "%j" parse "{11:22,33:44" # {11:22,33:44} "%j" parse "{'foo':22}" # {"foo":22} The "%J" format ((list 11,22),(list 33,44)) dict "one","two" # "{[11,22]:\"one\",[33,44]:\"two\"}" "%J" format insert a b with 11 22 33 44 end # "<\"a\":[11,33],\"b\":[22,44]>" "%J" format image[3,4] # "%%IMG0AAMABAAAAAA=" If any patterns specify names, the result of "%[one]i %[two]i" parse "34 56" # {"one":34,"two":56} "%[one]i %[two]i" format ("one","two") dict 34,56 # "34 56" The For example, we can use "Apple" like "A..le" # 1 "(555)-867-5309" like "(###)-###-####" # 1 We can use backtick escapes when we need to match against a special character: "2*3" like "#`*#" # 1 And we can use "The Best Orange" like "The*" # prefix "The Best Orange" like "*Best*" # infix "The Best Orange" like "*Orange" # suffix If the left argument to t:("widget","plastic dingus","whatsit","extruded plastic dingus","dingus") t like "*dingus" # (0,1,0,1,1) select where value like "*dingus" from t # +---------------------------+ # | value | # +---------------------------+ # | "plastic dingus" | # | "extruded plastic dingus" | # | "dingus" | # +---------------------------+ If the right argument to t:("apple pie","key lime pie","banana cream pie","apple computer") t like ("apple*","banana*") # (1,0,1,1) select where value like ("apple*","banana*") from t # +--------------------+ # | value | # +--------------------+ # | "apple pie" | # | "banana cream pie" | # | "apple computer" | # +--------------------+ Lil, the Vector LanguageLil has a number of features influenced by "Vector-oriented" languages like APL, J, K, and Q. The essence of vector-oriented languages is thinking about manipulating entire data structures at once, rather than manipulating their elements serially. The most obvious vector-oriented feature in Lil is conforming, in which a number of primitive operators like Let's start with a unary operator. Applied to a list, -(5) # -5 -(10,-35) # (-10,35) With a binary operator, a non-list item will "spread" and be paired with each element in a list. Given two lists, corresponding element from each list are "paired up": 100+10 # 110 100+(10,20) # (110,120) (100,200)+10 # (110,210) (100,200)+(10,20) # (110,220) The same pattern is carried out recursively; you can operate on arbitrarily high-dimensional data this way. Sometimes you may still need an 2*(list 1,2,3),(list 3,4) # ((2,4,6),(6,8)) 2 cross 3 # ((0,0),(1,0),(0,1),(1,1),(0,2),(1,2)) each x in 2 cross 3 (27,19)+x end # ((27,19),(28,19),(27,20),(28,20),(27,21),(28,21)) flip(27,19)+flip 2 cross 3 # ((27,19),(28,19),(27,20),(28,20),(27,21),(28,21)) (2 cross 3)+list(27,19) # (see below) # ((27,19),(28,19),(27,20),(28,20),(27,21),(28,21)) Conforming is why Lil has two different equality operators: 22=11,22,33 # (0,1,0) 22~11,22,33 # 0 (11,22,33)=11,22,33 # (1,1,1) (11,22,33)~11,22,33 # 1 ()=11,22 # () ()~11,22 # 0 In many situations, If two lists do not have the same length, Lil will truncate or repeat the right argument ( (11,22,33,44)+(100,200) # (111,222,133,244) (11,22,33,44)+(100,200,300,400,500) # (111,222,333,444) (100,200)+(11,22,33,44) # (111,222) This behavior can be useful for finding relative relationships in a list, comparing it to a "shifted" copy of itself: v:1,2,2,5,3,6,7,7 (1 drop v)=v # (0,1,0,0,0,0,1) # same as previous? (1 drop v)>v # (1,0,1,0,1,1,0) # strictly increasing? (1 drop v)-v # (1,0,3,-2,3,1,0) # relative change If you're familiar with Q or K, you might recognize this pattern as similar to the applications of the eachprior adverb. Another application is applying a "mask" pattern to an entire list: (11,22,33,44,55)*(0,1) # (0,22,0,44,0) # mask off odd items (11,22,33,44,55)*(1,0) # (11,0,33,0,55) # mask off even items Yet another useful consequence of this behavior is that enlisting the right argument when conforming two lists can be used to compute a matrix giving the cartesian product: every combination of the elements of the left and right lists. Compare this with (11,22,33) + 100,200 # (111,222,133) (11,22,33) + list 100,200 # ((111,211),(122,222),(133,233)) (100,200) cross 11,22,33 # ((100,11),(200,11),(100,22),(200,22),(100,33),(200,33)) sum @ (100,200) cross 11,22,33 # (111,211,122,222,133,233) You can also conform dictionaries. Lil takes the union of keys in the dictionaries and applies the operator between dictionary elements (or nil if the entry is missing). As when taking the union of dictionaries with x:("White","Brown","Speckled") dict 10,34,27 y:("Brown","White","Blue" ) dict 9,13,35 x+y # {"White":23,"Brown":43,"Speckled":27,"Blue":35} y+x # {"Brown":43,"White":23,"Blue":35,"Speckled":27} If you use a conforming operator between a dictionary and non-dictionary value, the non-dictionary value will be spread to every element of the dictionary and then conformed recursively as usual. Thus, if either argument is a dictionary, the result will be a dictionary: d: ("Alpha","Beta") dict (list 5,7),(list 3) d+100 # {"Alpha":(105,107),"Beta":103} (10,20)*d # {"Alpha":(50,140),"Beta":(30,60)} Combining conforming operators with reducing operators like needle: "apple" haystack: ("frog","apple","chicken","toadstool","apple","rice","fish") c:0 each x in haystack if x~needle c:c+1 end end c # imperative c:0 each x in haystack c:c+x~needle end c # imperative, without a conditional count extract value where value=needle from haystack # query count needle take haystack # functional (filter) sum needle=haystack # vector-oriented (spread-conforming =) Transforming iterative code into parallel, vector-oriented algorithms makes Lil much faster and more efficient, and may result in simpler and clearer code. Consider the following two approaches for replacing values in an array that are less than 5 with the number "99": each v in x # iterative loop if v<5 99 else v end end m:x<5 # compute a "mask" of 0 or 1 (99*m) + x*!m # multiply and add to combine masked and unmasked values The "ABC" @ 0,0,1,2,1,2,0 # ("A","A","B","C","B","C","A") ("AB" dict 11,22) @ "BAAB" # (22,11,11,22) If the left argument is a function, it is applied to each element of the right argument, like a more concise on triple x do x,x,x end each x in 11,22,33 triple[x] end # ((11,11,11),(22,22,22),(33,33,33)) triple @ 11,22,33 # ((11,11,11),(22,22,22),(33,33,33)) This also works if the "left argument" is a primitive unary operator, "pushing" the operator onto each element of a list or dictionary: first "Cherry","Olive","Orange","Lime" # "Cherry" first @ "Cherry","Olive","Orange","Lime" # ("C","O","O","L") count ("Alpha","Beta") dict (list 11,22,33),(list 44,55) # 2 count @ ("Alpha","Beta") dict (list 11,22,33),(list 44,55) # {"Alpha":3,"Beta":2} You can use multiple a:"%J" parse "[[[1,2],[3,4]],[[5,6],[7,9,10]]" count a # 2 count @ a # (2,2) count @ @ a # ((2,2),(2,3)) count @ @ @ a # (((1,1),(1,1)),((1,1),(1,1,1))) Lil, the Decker LanguageLil contains a number of language features and builtins specifically intended for interaction with the Decker environment. The The on go x do # provide our own novel 'special name': if x~"Random" x:random[deck.cards] end # use 'send' to call the original definition: send go[x] end An interface appears similar to a dictionary, but indexing or assignment through an interface may have side-effects, and the values in fields may change over time: sys # <system> "%e" format sys.now # "2021-11-12T02:00:59Z" "%e" format sys.now # "2021-11-12T02:01:07Z" time:sys # <system> time.now # 1636682495 Interfaces cannot be defined from Lil programs- they are furnished by a host application like Decker. Consult Decker's manual for a description of the interfaces you can use in your scripts. Many "utility" interfaces offer considerably more efficient ways of performing operations than with raw Lil alone:
It is not possible to enumerate the keys of an interface; they may have infinitely many keys, populated on the fly. Thus, the Interfaces can be compared with sys~sys # 1 sys=(sys,sys,123) # (1,1,0) All interfaces will accept the sys.type # "system" (sys,sys,sys)..type # ("system","system","system") Accessing an invalid index will return nil, and attempting to write to an invalid (or read-only) index will return the expression to the right, like any other indexed assignment: sys.bogus # nil sys.bogus:123 # 123 Appendix 1: Unary PrimitivesThe unary arithmetic primitives
The unary aggregation primitives The
Appendix 2: Binary PrimitivesThe binary arithmetic and comparison primitives The modulus operator The The The
Note that if you represent sets as dictionaries, the x:"AB" dict () # {"A":nil,"B":nil} y:"BC" dict () # {"B":nil,"C":nil} (keys x) take y # {"B":nil} (intersection) (keys x) drop y # {"C":nil} (disjunction) x,y # {"A":nil,"B":nil,"C":nil} (union)
Appendix 3: Language GrammarThe following is a slightly hand-waved EBNF description of Lil's syntax. The production MONAD := '-'|'!'|'floor'|'ceil'|'cos'|'sin'|'tan'|'exp'|'ln'|'sqrt'|'count'|'first'|'last'|'sum'|'min'|'max'|
'raze'|'prod'|'range'|'keys'|'list'|'rows'|'cols'|'table'|'typeof'|'flip'|'mag'|'unit'|'heading'|
'trim'|'rev'|'distinct'
DYAD := '+'|'-'|'*'|'/'|'%'|'^'|'<'|'>'|'='|'&'|'|'|','|'~'|'@'|'split'|'fuse'|'dict'|'take'|'drop'|
'in'|'join'|'cross'|'parse'|'format'|'unless'|'limit'|'like'|'window'|'fill'
DIGIT := '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
NUMBER := '-'? DIGIT+ '.'? | DIGIT* '.' DIGIT+
STRING := '"' (NON_ESC|'\\'|'\"'|'\n')* '"'
LITERAL := NUMBER | STRING | '(' ')'
NAME := (ALPHA|'_'|'?') (ALPHA|'_'|'?'|DIGIT)*
ITER := (( each' NAME* 'in' ) | 'while') EXPR* 'end'
ON := 'on' ( '...' NAME | NAME+ ) 'do' EXPR* 'end'
IF := 'if' EXPR* ('elseif' EXPR*)* ( 'else' EXPR* )? 'end'
CLAUSE := ('where' EXPR) | ('by' EXPR) | ('orderby' EXPR ('asc'|'desc'))
QUERY := ('select'|'extract'|'update')((NAME|STRING ':')?EXPR)* CLAUSE* 'from' EXPR
INSERT := 'insert' (NAME|STRING)* 'with' EXPR* (('into' EXPR) | 'end')
SEND := 'send' NAME '[' EXPR* ']'
INDEX := ('.' NAME? | '[' EXPR* ']')*
ACCESS := NAME INDEX ( ':' EXPR )?
LOCAL := 'local' NAME ':' EXPR
TERM := LITERAL | ITER | ON | IF | QUERY | INSERT | SEND | '(' EXPR ')' | MONAD EXPR | ACCESS | LOCAL
EXPR := TERM ( INDEX ( ':' EXPR )? | DYAD EXPR )?
PROGRAM := EXPR* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |