Skip to content

Language Specification

Neuron Teckid edited this page Mar 21, 2016 · 5 revisions

注释

行末注释

在行间任何位置加入井号 #, 此后直到此行换行符均视作注释, 如

console.log(0) # log "0" in console

保留字

语言关键字

func ifnot if else return extern export typeof true false try catch throw
class this super ctor break continue for enum include

其它保留字

from finally with yield while gen delete

宏是部分保留字. 所有的双下划线开头, 双下划线结尾, 长度超过 4 的标识符都是宏, 不应该用于变量定义, 但是可以作为成员名.

表达式

标识符

标识符用于表示一个值的引用, 形式参数, 对象属性等.

标识符的构成是, 以下划线或字母开头, 后跟任意多个下划线或数字或字母的非保留字.

字面常量

Flatscript 支持的字面常量包括

简单字面量

  • 十进制整数, 表示为为至少 1 位 0-9 数字, 如 123456789, 42; 在各位数字之间可以插入下划线来分隔数字, 使数字显得更加易读, 如 1_234_567_890
  • 十进制小数, 表示为任意多位 0-9 数字后 1 个小数点, 之后至少 1 位 0-9 数字, 如 3.14, 2.718, .707; 在各位数字之间可以插入下划线来分隔数字, 使数字显得更加易读, 如 1_234_567.891_234
  • bool 类型值, 字符串 truefalse
  • 字符串, 以双引号 " 或单引号 ' 引起的字符序列, 字符序列中如果出现了引号, 反斜杠 \ 或其它需要转义实现的字符, 在该字符之前加上一个反斜杠. 可转义的字符包括: 单引号由 \' 转义, 双引号由 \" 转义, 反斜杠由 \\ 转义, 换行符由 \n 转义, 制表符由 \t 转义, 如 "Hello, World!", 'Hello,\nWorld!'; 或以三个双引号 """ 或三个单引号 ''' 引起的字符序列, 此时字符序列中如果出现单个或两个引号, 无须转义, 如 '''<input type='text' value=''>'''

简单字面量的单目运算或简单字面量之间的双目运算将在编译期进行常量折叠. 如果发现不支持的运算将在编译时报错, 如将常量设置为 instancof 的右值.

简单字面量和它们经过算数运算, 比较运算, 逻辑运算得到的结果均为 编译时可推导的.

列表常量

表示为方括号 [ 开始, 反方括号 ] 结束, 其中任意多个表达式, 每两个表达式之间用逗号隔开, 如 [ 2, 3, 5, 7, 11, 13 ].

对象常量

表示为大括号 { 开始, 反大括号 } 结束, 其中任意多个属性名-属性值对, 每两个名-值对之间用逗号隔开, 名-值对表示为冒号隔开的标识符 (或简单字面量) 与表达式, 如 { hello: 'world', good: 'day' }; 最后一个名-值后可以添加一个额外的逗号, 如

{
    hello: 'world',
    'good': 'day',
}

宏是编译器根据上下文或编译参数不同而预设的一些常量, 在不同上下文中它们可能取值不同, 但都会被视作编译时可推导的常量. 详见 .

引用

表达式中的标识符表示对一个值的引用.

若标识符引用的是编译时可推导的, 则该标识符也视为编译时可推导的.

正则表达式

同 Javascript 中的正则表达式.

管道对象

写作 $, $i, $k 的词法元素. 参见之后 "管道" 一节.

异常对象

写作 $e 的词法元素, 参见之后 "异常处理" 一节.

算术运算

包括一元正 (+), 一元负 (-), 加法 (+), 减法 (-), 乘法 (*), 除法 (/), 取模 (%), 书写方式与数学一致.

一元算术运算符优先级最高, 乘除取模次之, 加减最低.

列表连接

算符为 ++, 二元运算, 将两个列表中的元素顺序连接构成新的列表.

优先级等同于加减运算符.

位运算

包括取反 (~), 与 (&), 或 (|), 异或 (^), 左移 (<<), 右移 (>>), 无符号右移 (>>>).

其中取反优先级与一元算术运算符相同, 二元算术运算优先级低于加减运算符.

关系运算子

包括

  • owns: hasOwnProperty 的简写, 如 a owns b 等价与 a.hasOwnProperty(b)
  • instanceof: 同 JS

关系运算子的优先级低于二元位运算符

比较运算

包括等于 (=), 大于等于 (>=), 小于等于 (<=), 大于 (>), 小于 (<), 不等于 (!=).

比较运算符优先级低于关系运算子.

逻辑运算

包括非 (!), 与 (&&), 或 (||).

当与运算左参数值为假时, 表达式的值为假; 当或运算左参数值为真时, 表达式的值为真. (这些情况运行时均跳过右参数计算)

非优先级最高, 与次之, 或最低.

逻辑运算符优先级低于任何比较运算符.

条件选择运算

条件选择运算语法形如

consequence if predicate else alternative

其中 predicate 为条件, 若条件为真值, 则表达式的值同 consequence, 否则同 alternative. 运行时仅计算 consequencealternative 两者之一.

三个子表达式均不得直接递归地包含另一个条件选择表达式, 即下面的表达式不合法

a if b if c else d else e
a if b else c if d else e

但可以括号间接地包含另一个条件选择表达式, 如

a if (b if c else d) else e
(a if b else c) if d else e
a if b else (c if d else e)

条件运算符优先级低于任何逻辑运算符.

成员查找

表达式之后通过方括号查找集合中的成员, 该表达式应该是列表或字典这样的集合类型. 如

x: ['a', 'b', 'c', 'd']
y: x[0]
m: { 'first': 0, 'second': 1, 'third': 2 }
n: m['first']

此时 y 的值为 'a', m 的值为 0.

列表切片

Flatscript 提供了列表切片表达式, 在列表对象后方括号中放入 2 至 3 个由逗号隔开的值表示, 如

x: ['a', 'b', 'c', 'd']
a: x[2,]
b: x[,2]
c: x[,,2]
d: x[,,-1]

此时 c 的值为 ['c', 'd'], b 的值为 ['a', 'b'], c 的值为 ['a', 'c'], d 的值为 ['d', 'c', 'b', 'a']

其中, 第一个数值表示开始索引, 第二个表示结束索引 (不包括在内), 第三个表示步长; 若这些成分部分省略, 则

  • 步长默认为 1
  • 步长为正值时, 默认起始索引为 0, 否则为列表长度减 1 (即从最后开始)
  • 步长为正值时, 默认结束索引为列表长度, 否则为 -1 (即到数组开头为止)

管道与管道对象

管道是一系列表达式加上管道分隔符构成, 每一个表达式称为管道的 "节".

管道包括映射管道与过滤管道. 映射管道分隔符为 |:, 过滤管道分隔符为 |?.

管道的首节应该是一个类型为列表的对象.

映射管道将首节列表或对象中每个对象或属性按照指定表达式的映射方式, 生成一个新的列表, 此列表称为迭代结果, 是管道表达式的值. 如

[1, 3, 5, 7] |: $ * $

{
    Monday: 1,
    Tuesday: 2,
    Wednesday: 3,
} |: $k + ' is workday.'

结果分别是

[1, 9, 25, 49]
[ "Monday is workday", "Tuesday is workday", "Wednesday is workday" ]

其中 $ 表示每次迭代的对象, $k 表示属性名. 可用的管道对象包括

  • $ 表示迭代对象
  • $i$index 表示迭代索引
  • $k$key 表示对象迭代的属性名 (在异步管道中, 该对象值将为 null)
  • $r$result 表示迭代结果

过滤管道则是从列表中筛选出满足条件的对象, 构成新的列表, 如

[1, 3, 5, 7] |? $ % 3 != 0

则结果是

[1, 5, 7]

管道分隔符优先级低于条件选择运算.

另参见将语句块作为管道体.

当一个语句只含有一个管道时, 该管道为管道语句. 管道语句中可以使用 returnbreak.

函数调用

将表达式实参列表附加在引用之后表示调用函数, 如

nop()
fib(x + y)
add(1, 2, 3)

书写方式与数学一致. 可在最后一个实际参数之后多出 1 逗号.

父类函数调用

在类的成员函数或构造函数的函数体内调用该类型的基类的指定成员函数. 形式必须是

super.BASE_CLASS_FUNCTION_NAME(ARGUMENTS)

BASE_CLASS_FUNCTION_NAME 必须是一个标识符, ARGUMENTS 部分同普通函数调用形式参数列表.

详见类和继承.

行间 lambda

Lambda 即匿名函数, 行间形式以形式参数列表-冒号-表达式构成, 如

(x): x * x

该形式下, 表达式即视作函数的返回值, 如

[2, 3, 5, 7].map((x): x * x)

得到 [4, 9, 25, 49].

前置 * 操作符

使用前置 * 操作符表示 Javascript 中的 new 操作符. 如

x: 'Javascript'.replace(*RegExp('Java', 'g'), 'Flatscript')

多行 lambda

为了弥补行间匿名函数在表达力上的不足, Flatscript 允许定义多行 lambda. 在匿名函数的冒号后折行, 此后的若干缩进相同且缩进多于该表达式所属的行 (详情参见下面语句缩进规则) 的连续多行均视作该匿名函数的函数体. 如

[2, 3, 5, 7].map((x):
        return x * x
    )

或又如

setTimeout(():
    do_a()
    do_b()
, 2000)

缩进与多行 lambda 的进一步规则在下面折行规则之后会进一步详细说明.

获取类型

在表达式之前如前置单目运算符一样放置 typeof 表示获取该对象类型表示的字符串.

对于简单字面量或其引用而言, typeof 能在编译时给出常量类型字符串.

this

this 关键字. 详见 This 引用.

异步占位符

异步调用.

语句

缩进语法形式

Flatscript 以缩进 (处于行首的空白) 标记函数或分支语句的开始与结束, 相同缩进的语句会被认为在同一个块中, 比如

func add(a, b)
    return a + b
console.log(add(10, 5))

其中 func add(a, b) 是一个函数定义的开头, return a + b 为该定义的函数体中的语句. 而接下来的一句 console.log(add(10, 5)) 缩进级别与 func 相同, 故不属于上述函数.

同一区块的语句缩进必须相同, 不能出现如下情况

console.log(0)
 console.log(1)

缩进不能出现 tab 字符.

算术语句

此语句用于单纯放置一个表达式, 该表达式中的值会被计算 (但不会被使用).

名字定义

名字定义格式如下

标识符 : 表达式

表示用该标识符定义一个局部名字. 表达式中直接使用该名字, 即视为对冒号后表达式的值的引用.

名字一旦定义, 不能在相同空间内定义同样名字的其他值, 也不能修改此定义所引用的值.

枚举

使用关键字 enum 可以定义一系列递增的 编译时可推导的 常量, 如

enum X, Y, Z

X, Y, Z 的值分别是 0, 1, 2. 同一个 enum 内, 第一个定义的名字的值为 0, 之后每定义一个名字, 其相对于之前定义的一个名字的取值增加 1.

枚举语句支持在定义的名字后的逗号处折行, 如

enum A, B,
     C, D

是一个完整的语句.

域级函数定义

域级函数定义以关键字 func 开头, 后接函数名 (须是一个标识符), 之后为括号括起的形参列表, 如

func factorial(x)

接下来的语句为函数体. 函数体中每一条语句须多 1 级缩进, 若有子函数或分支语句, 则它们的子句继续缩进, 如

func factorial(x)
    if x < 2
        return 1
    return factorial(x - 1) * x

函数定义不可以作为分支语句的字句.

属性设置

在 Flatscript 语言中, 名字一旦定义就无法再次修改其引用值. 但可以通过属性设置的方式来修改该名字所引用的表达式的属性值

object.attribute: expression
object[attribute]: expression

此语法唯一区别于名字定义的是, 冒号左边的表达式不是一个单独的标识符. object 是任意的表达式; 第一种写法中 attribute 必须是标识符, 后一种写法中, attribute 可以是任意表达式.

域级类定义

类定义必须在域中, 不允许匿名类型. 定义了类之后, 该域空间内可以引用该类名. 但该类型定义之前, 其它类不能使用该类作为父类.

语法和规定详见类和继承.

函数返回

函数计算得出结果, 或者需要终止时, 由返回语句交给函数调用者其结果. 返回语句有下面两种形式

return
return 表达式

前者返回空值到调用者, 而后者将表达式的值返回给调用者.

返回语句可以用于在同步函数调用中直接返回结果, 或在异步环境中以返回表达式作为参数值调用回调函数, 参考方法挑选.

分支

判断当某条件成立时选择执行的代码, 如

if x < 0
    return 0

分支语句的子句缩进要较 if 所在行多 1 级.

如果要表示在上述条件不满足时执行某些语句, 有如下写法

if x < 0
    console.log(0)
else
    console.log(x)

这表示当分支条件成立时执行某些子句, 而不成立时执行另一些子句; 或者, 用如下写法

ifnot x < 0
    console.log(x)

这样表示分支条件不成立时执行某些子句 (这种形式不支持 else 分句).

基于范围的循环

基于一个数值范围的循环, 语法如

for NAME range BEGIN, END, STEP
    LOOP_BODY

其中 for 是关键字, 但 range 不是关键字. NAME 必须是一个标识符, 指出循环变量名, BEGIN 是起始值, END 是阈值, 达到或超过则循环终止, STEP 是步长, LOOP_BODY 为一个语句块.

步长值 STEP 必须为一个可编译时推导的非零数值, 当其为正数时, NAME 对应的取值每次递增, 直到大于或等于 END 的值时循环结束; 当其为负数时, NAME 对应的值每次递减, 直到小于或等于 END 的值时循环结束.

BEGIN, STEP 可以省略. 当 range 之后只有一个表达式时, 该表达式为 END, BEGIN 默认为 0; 当 range 之后只有两个表达式时, 它们依次是 BEGINEND, STEP 默认取值为 1.

for i range 10
    console.log(i) # 0 ... 9

for i range 2, 10
    console.log(i) # 2 ... 9

s: -1
for i range 10, 0, s
    console.log(i) # 10 ... 1

由于 range 并不是一个保留字, 在此语法中只起到标识作用, 故以下写法是合法的

range: 10
for i range range
    console.log(i) # 0 ... 9

循环控制

使用 break 中断循环, 使用 continue 进行下一次循环. 这两种语句应该以单一关键字占一行的形式给出.

for i range 10
    if i % 3 = 0
        continue
    console.log(i)

['ab', 'cd', 'efg', 'hij'] |:
    if $.length = 3
        console.log($)
        break

异常处理

捕获指定语句块中的同步或异步语句产生异常, 如

try
    any_statement
catch
    do_something_with($e)

$e 表示此过程中被捕获的异常对象. $e 不可以在 catch 块外被使用.

详见异常处理.

抛出异常

产生一个异常, 终止执行流. 语法为

throw 表达式

在许多其他语言中支持的异常重抛出, 即类似 throw 关键字但不带表达式在 Flatscript 中不被支持, 因为在异步环境下调用栈很难被正确地体现.

抛出异常语句可以用于在同步函数调用中直接抛出异常, 或在异步环境中以异常表达式作为参数值调用回调函数, 参考方法挑选.

捕获异常

使用 try-catch 捕获任何发生在 try 直属语句块内的异常. 语法格式为

try
    语句块
catch
    语句块

直属语句块 中的语句指在 try 内的所有的变量声明, 属性设置, 分支子句, 或嵌套的 try-catch 子句, 返回值, 异常抛出, 同步或异步的函数调用, 管道, 但不包括内嵌的匿名函数的函数体. 在 samples/try-catch.stkn 中有许多关于捕获函数内抛出异常的例子, 而以下写法中, 匿名函数函数体中抛出的异常则不能被捕获

try
    setTimeout(():
        throw 'some exception'
    , 1)
catch
    console.log('caught')

catch 语句块中, 可以引用 $e 作为捕获的异常的值. 该异常可能由 throw 语句抛出, 也可能是正规异步调用的回调函数的错误参数值.

外部名字引入

当文件中文件引用的名字的定义并不由该模块本身提供, 则应通过引入语句将名字添加到上下文

extern name
extern name_0, name_1, name_2

以上两个引入语句, 分别引入了名为 name 的名字, 与名为 name_0, name_1, name_2 3 个名字.

下面的名字会被默认定义在全局空间

console setTimeout setInterval clearTimeout parseInt parseFloat Number Date
Math Object Function escape unescape encodeURI encodeURIComponent decodeURI
decodeURIComponent JSON NaN null undefined isFinite isNaN RegExp

如果要在编译前引入其它名字, 可通过 -e 命令行参数, 如

$ flatsc -e document -e window < input.stkn > output.js

名字导出

常见于浏览器脚本中, 如果要让某个值暴露到全局空间或暴露为模块接口, 需要导出该名字 (否则定义的名字均仅为当前文件空间可见)

export export_point: value
export ident_a.ident_b.ident_c: value

导出点必须是标识符, 或点号连接起来的多个标识符. 如 ab, ab.cd.ef 都是合法的, 而 ab['cd'].ef 不合法.

对比于属性设置, 导出语句会协助确保导出点的合法性. 生成的 Javascript 代码不是一个单一的赋值. 如

export util.strings.escape: escaping_func

所生成的代码将类似

$export.util = $export.util || {};
$export.util.strings = $export.util.strings || {};
$export.util.strings.escape = escaping_func

其中 $export 将会根据情况设置为 windowmodule.exports.

加载文件

参见文件加载语句.

名字空间与查找

在 Flatscript 语句中, 任何引用都会在编译时检查其是否在上下文中有定义. 如下面的完整程序编译时会产生名字查找错误

http: require('http')

因为 require 并没有在上下文中定义. 若要修改, 可改为

extern require
http: require('http')

这样将在名字空间中引入外部定义的 require, 在其后的代码中方可引用之.

名字空间

一组缩进相同语句, 从名字定义的角度上称为 名字空间, 在名字空间中定义或导入的名字可以在该定义或导入语句之后被引用, 如

func echo()
    hello: 'Hello, world!'
    console.log(hello)

但在该语句之前不应引用, 如

func echo()
    console.log(hello)
    hello: 'Hello, world!'

console.log(hello) 一句会提示 console 未定义.

函数定义不受此限制, 函数可以在定义处前被使用, 如

func echo()
    hello: 'Hello, world!'
    console.log(hello)

echo()

等价于

echo()

func echo()
    hello: 'Hello, world!'
    console.log(hello)

但是, 若无特殊需求, 最好将函数定义置于名字空间最开始处.

而通过名字定义引入的匿名函数则例外, 如上述功能可作如下改写

echo: ():
    hello: 'Hello, world!'
    console.log(hello)

echo()

但不能改写成

echo()

echo: ():
    hello: 'Hello, world!'
    console.log(hello)

名字注册

在名字空间中, 产生可以引用的名字共有下面方法

  • 定义名字语句 (定义的名字)
  • 名字导入语句 (导入的名字)
  • 域级函数定义 (函数名, 将在整个名字空间中可见, 无论对该函数的引用在函数定义之前还是之后)
  • 函数形参列表 (中的每个参数名)
  • 异步占位符中的形式参数列表 (中的每个参数名)
  • 加载文件作为模块

同一名字空间内, 相同名字不得重复这册.

引用外部空间

语句除了可以引用当前名字空间中的名字, 还可以引用其所在名字空间外层名字空间中的名字, 如

func external_space(a)
    func internal_space(b)
        return a + b
    return internal_space(42)

internal_space 中, 可以引用 external_space 中的名字 a (以参数形式注册).

当内层空间注册了与外层空间相同的名字时, 内层空间中的名字会覆盖外层空间中的名字, 如

func external_space(a)
    func internal_space(b)
        a: 0
        return a + b
    return internal_space(42)

console.log(external_space(1))

这段代码执行的结果将是 42 而不是 43, 因为 internal_space 中定义的 a 覆盖了 external_space 形参 a.

如果在内层空间先引用了外层空间的名字, 然后内层空间又定义了该名字, 则编译时会产生一个错误, 如

func external_space(a)
    func internal_space(b)
        x: a
        a: 0
        return x + b
    return internal_space(42)

折行规则

在编写程序时, 如果一行内容过于长, 建议将一行拆分为多行书写. 如

long_statement: expression_0 * expression_1 + expression_2 * expression_3

可写作

long_statement: expression_0 * expression_1 +
            expression_2 * expression_3

但由于 Flatscript 的缩进语法特性, 折行受到一些语法限制, 如下面的写法

if expression_0
    + expression_1 * expression_2

无法被正确地解释语法; 否则, 可将 expression_0 + expression_1 * expression_2 解释为 if 的条件, 另外可以将 expression_0 解释为 if 的条件而 +expression_1 * expression_2 被解释为第一条语句. 下面将描述 Flatscript 中折行的规则

可折行处

折行均应用于表达式中.

双目运算

表达式中, 在任何双目运算符 (四则运算符, 比较运算符等) 后可以折行, 如

long_statement: expression_0 * expression_1 + expression_2 * expression_3

等价于

long_statement: expression_0 * expression_1 +
            expression_2 * expression_3

也等价于

long_statement: expression_0 * expression_1 + expression_2 *
            expression_3

不可写为

long_statement: expression_0 * expression_1
            + expression_2 * expression_3

成员访问 (点号 .) 与管道操作符在折行发生时均视作双目运算符.

带有括号的情况

函数调用的实参之前, 或列表字面常量的每个表项表达式之前都可以折行, 如

Math.max(expression_0, expression_1)

等价于

Math.max(expression_0,
    expression_1)

也等价于

Math.max(
    expression_0, expression_1)

或如

[expression_0] ++ [expression_1, expression_2,  expression_2]

等价于

[expression_0] ++ [expression_1,
            expression_2,  expression_2]

凡是括号未配对时, 表达式中任何一处均可折行. (函数定义头除外, 但匿名函数的形参列表中可以折行)

折行后的缩进

折行后, 开头的空格将被忽略, 此行的内容将视作上一行的延续.

匿名函数的冒号后换行

此情况并不视作折行. 此处换行后, 其后一行缩进满足较上一行语句增加, 则从折行处开始, 所有与折行处语句缩进相同的语句会计入该匿名函数的函数体. 如

x: (a, b):
    document.getElementById('a').innerHTML: a
    document.getElementById('b').innerHTML: b
y: x('shinto', 'shrine')

其中两个名字定义语句是同一空间的, 而两个属性设置语句是匿名函数的函数体.

函数体中的语句必须满足

  • 各语句缩进相同
  • 它们的缩进必须多于匿名函数形参列表所属的语句的缩进

在函数体开始之后, 当接下来的语句 (或片段) 的缩进少于匿名函数体中语句的缩进时, 匿名函数的函数体立即视作结束. 此时的语句将根据上一行结束与否, 作为独立语句编译, 或作为上一行的延续. 如上面的例子, 因为名字 x 的定义语法上可视作正常结束, 因此下面出现的 y: x('shinto', 'shrine') 作为独立的语句编译.

而下面的例子则相对地, 后续的内容作为前一个语句的延续

setTimeout(():
    document.getElementById('a').innerHTML: a
    document.getElementById('b').innerHTML: b
, 2000)

setTimeout(():
    document.getElementById('a').innerHTML: a
    document.getElementById('b').innerHTML: b
  , 2000)

setTimeout((): 一行在匿名函数函数体结束后并不能结束, 因此下面出现的 , 2000) 将作为这个语句的后续部分编译.

或下面这个例子

operators: {
    plus: (a, b):
        return a + b
      ,
    minus: (a, b):
        return a - b
      ,
}

其中的逗号缩进少于匿名函数体, 表示匿名函数的结束, 而此逗号将被对象常量解释, 视作属性名-属性值对的分隔符.