LoveString基础
注意:我不打算实际构建这门语言!
设计目标
完全基于Dictionary、仅有String类型的语言,无敌炫酷的玩具。
先来个Hello World吧!
func init:
print: "Hello World!"
从了解符号开始
两种引号,
[]
围住的部分为Key
,""
围住的部分为Value;=
作为赋值符号使用。#
与LF
围住的部分将被视为注释。%
为“Value化”符号,用于转义和格式字符串。print: "%"%"" #""
print: "My name is %s, welcome to learn %s%s." % "John""Love""String"
# My name is John, welcome to learn LoveString.
()
围住的部分称为优先行,用于提高运算优先级,以及跨行书写同一语句;优先行会尝试捕获一个Value。Space
用于分隔关键字。因此,各个关键字之间必须存在空格(除非换行)。LF
用于分隔语句(即:每行代表一个语句),Tab
用于表明代码的局部级别。要求以
:
作方法关键字的后缀,唯一例外是func:
(定义一个匿名函数)
除=
为从右向左结合,其余关键字全部为从左向右结合。
LoveString的字典
入手字典,先从Key
和Value
开始。可以简单地把Key
理解为变量名,Value
理解为变量值,但实际上更为简单。
一般的字典携带一系列的键值对
或键键对
,现在我们来定义一个一般的字典:
root[My First Dictionary] = {
[1] = "Love"
[2] = "String"
}
root
关键字代表对根字典进行操作,后面我们会对根字典作出详细的说明。
由于空格分词,{
不要换行;由于字典结束是一个单独的语句,}
需要换行。
添加键,删除键
不存在的Key对应的Value是""
,即空字符串。因此:
添加一个键,只需访问一个不存在的键,然后赋值;
删除一个键,只需将一个存在的键赋为空值。
示例如下:
func init:
root[my_new_key_1] = "Love"
root[my_new_key_2] = "String"
root[to_be_erased] = "Foobar"
root[to_be_erased] = ""
print: [undefined_key] #输出一个空行
方法
方法是代码段的封装,可以用于支持外部语言、实现代码复用、标记程序入口等。现在我们来定义一个一般的方法:
func my_method: [var1]
[var2] = "String"
print: ([var1] + [var2])
然后执行它:
func init:
my_method: "Love"
# LoveString
从定义过程中可以看出,方法包含一个自定义关键字、一个内部字典和一系列的语句。以func
开头的行,将检测第一个空格后的关键字名,并对第二个空格后的键进行“导出”。
执行方法时,内部字典从空字典开始,先将传入的键/值写入内部字典,然后执行其后的一系列语句。最后,把内部字典全部置空。
使用return关键字能终结方法的执行,并“导出”一个局部变量,这点与其它语言是一致的。
特别地,使用return_self
可以保证方法“逐级蚕食”后面的方法,理解这点需要一些匿名函数的知识。
键名是静态的
遇到这样一个代码段怎么办?
func my_add_root: [sth]
root[sth] = [sth]
答:将传入的[sth]键/值写入根字典的名为sth
的键。
我们强烈不建议用动态键名扩充字典,但真的想这么做的话,可以使用add
家族的方法:
func my_dynamic_add: [name][value]
add_root: [name][value]
add_to: root[dynamicadds][name][value]
func init:
my_dynamic_add: "my_new_key_1""Love"
my_dynamic_add: "my_new_key_2""String"
print: (root[my_new_key_1] + root[dynamicadds][my_new_key_2])
# LoveString
add_root方法取第一个参数的最终值作为键名,将第二个参数以引用形式存储到根字典中。
add_to方法相较第一个方法在一开始多一个“目的地”。
默认值参数
考虑LoveString“未定义即空”的特性,我们允许执行方法时传入任意项参数,但需要注意赋值的目标问题:
func 123: [1][2][3]
root[1] = [1]
root[2] = [2]
root[3] = [3]
func init:
123: "Love""String"
print: (root[1] + root[2]) #LoveString
print: (root[1] + root[3]) #Love
123: "Love""""String"
print: (root[1] + root[2]) #Love
print: (root[1] + root[3]) #LoveString
123: "Love""""""String" #在这里,"String"被忽略了
print: (root[1] + root[2]) #Love
print: (root[1] + root[3]) #Love
为此,我们提供一个小小的语法糖,方便定义函数的默认值:
func 123: [1][2][3]
default[1] = "Love "
default[2] = "String "
default[3] = "Forever"
return ([1] + [2] + [3])
效果如下:
func init:
print: (123:) #Love String Forever
print: (123: "Love""String") #LoveString Forever
print: (123: "Love""String""") #LoveString
传入一个方法,还是一个值?
上一节中,您可能已经注意到我们对于嵌套方法的处理方式了:总是用优先行围住先执行并传值的方法。
如果不这样做会怎么样?下面给出一个有趣的例子:
func sth_lambda: [1]
print: "I am a Lambda!"
print: [1]
func init:
print: sth_lambda: "Not Ignored"
# ----------------
# func sth_lambda: [1]
# print: "I am a Lambda!"
# print: [1]
# ----------------
# Not Ignored
在此例子中:
我们向print:
方法传入了sth_lambda
方法,于是print
方法打印出了sth_lambda
方法的定义,并返回了一个print:
匿名函数。
之后,新的print:
匿名函数与字面量"Not Ignored"结合,于是打印出了一行"Not Ignored"语句。
怎么调用一个传入的方法呢?这里也只需要使用优先行:
func method_to_be_called:
print: "Called"
func method_passer: [method]
if ([method] isfunc) -> func:
([method]) #这样书写,传入的方法就会被执行
root[method] = [method]
else -> return
func init:
method_passer: method_to_be_called:
(root[method])
# Called
# Called
之后再展开讲解程序的选择与循环结构。
传入两个方法,还是遵照传递性进行运算?
始终记住,分词器依赖空格工作,方法的各个参数之间不用空格分割。
因此:
myfunc: func1:func2:
是向myfunc:传入两个方法;
myfunc: func1: func2:
是向myfunc:传入方法func1:
,如果返回了方法,则向返回方法传入func2:
;否则func2:
将被无视。
print:
在设计时同时支持return_self
式的运算符重载以及任意数量的可变参数,因此print:
后各个参数间写空格与不写空格等效。
值传递与引用传递(了解即可)
阅读到这里就可以展开讲解参数传递方式了。
LoveString对于“需要生成新值”的行为采取值传递,即传递字典的值;
对于其他行为,如赋值、函数传参,则使用引用传递。
如何实现引用传递呢?这里将介绍我们的内存模型。
程序初始状态下存在一个root_map,存储了Key的hash和字面量/字典的引用。赋值过程的流程如下:
判断赋值动作是
Key-Key
型,Key-Value
型,还是Key-Dict
型Key-Key
型:对右侧Key进行hash处理,遍历root_map来获得该key对应的引用;然后对左侧key进行hash处理,在root_map新增左侧Key-hash
的记录;Key-Value
型:开辟内存写入Value,然后在root_map新增一条记录;Key-Dict
型(将键与字典字面量绑定):为该字典字面量开辟submap,进行Value写入;root_map内新增内容为Key-Hash、submap引用
的记录。
每个Value和Submap均绑定一个引用计数。每个赋值动作均会导致对旧Value/Submap和新Value/Submap的引用计数变化与检测。引用计数归零时,该Value/Submap会被释放;Submap的释放过程中,其内各Value的引用计数均会减一。
循环引用问题(了解即可)
考虑如下的代码段:
func init:
root[A] = {
[sthB] = ""
}
root[B] = {
[sthA] = root[A]
}
root[A][sthB] = root[B]
这个代码段将按以下顺序工作:
root_map中新建
A
,然后构建Submap(A),内含的sthB
指向空字符串。root_map中新建
B
,然后构建Submap(B),内含的sthA
对应了Submap(A)的引用。修改Submap(A)中的
sthB
使包含对Submap(A)的引用。
这时两个Submap彼此引用,导致引用计数无法归零。为了解决这样的循环引用问题,每个Map(root_map或Submap)会预先包含一个SubmapList供赋值时检查。
循环引用的赋值时检查仅在Debug模式工作。
检查到循环引用时,程序会形成一个断点。请手动修复这样的循环引用问题。
根字典
在从了解符号开始
一节中,提到了用func:
定义匿名函数的方法。现在我们可以对字典的定义作一点简单的扩充:
字典是一个记录了一系列Key - Value引用
的Map(root_map或Submap),Key是一个hash化的名称,Value包含了对String字面量的引用、对Submap的引用、匿名函数,三者之一。
到此,可以给出根字典的定义:
根字典是字典的超集。除了Key - Value
关系表之外,根字典允许您使用func
定义具名方法。因此,具名方法不能嵌套定义。此外,根字典自带一个叫init:
的虚方法,其在全局变量全部加载完成后自动执行,作为程序入口而存在。
init:
虚方法可以携带参数。
LoveString将每个程序文件视为一个根字典,没有通过脚本新建根字典的方法。
运算符
作为高级语言,总不能仅仅赋一堆值就完事了吧?
于是我们尝试引入一些运算符
。在此提醒两点:
运算符是特殊的关键字,空格不能少!
关键字出现结合失败的情形时,其后的关键字和元素都会被忽略。
上文介绍了%
的两种用法,在这里作出简单的补充:
%% :形成一个百分号
%" : 形成一个引号
%n :换行
%t :Tab缩进
%s :字符串替换符
对%s作格式替换的例子:
[sth] % "Value1"[Value2][Dict3](func4:)func5:func6:
依次为:String字面量"Value1",[Value2]对应的值,[Dict3]对应Submap的文本输出,方法
func4:
的值,func5:``func6:
的定义。
接下来引入几种布尔运算符(优先级上高下低左高右低):
==
!=
:等值检测and
or
not
:逻辑与/或/非in
:若前者为Key,后者对应字典,且后者对应的字典包含了该Key,则返回true
。使用
self
访问方法的内部字典。isstr``isdict
isfunc
:类型判断,分别对应String/字典/函数isroot
:检查根字典是否有对该字面量的引用isroot_deep
:检查根字典及其子字典是否有对该字面量的引用(很慢)
对于布尔运算符的返回值,作出如下约定:
空字符串代表“非”,含内容的字符串代表“是”(一般记录为"true")
关键字
false
代表“非“,true
代表“是”
高级优先行
高级优先行
是特殊的优先行,是LoveString扩展模块的有力语法支撑。官方计划维护以下几种高级优先行:
group()
是字典结构的语法糖:
root[current_color] = group("1.0""1.0""1.0""1.0")
# 相当于
root[current_color] = {
[1] = "1.0"
[2] = "1.0"
[3] = "1.0"
[4] = "1.0"
}
此外,group()
为方法提供了可变参数支持:
func grouped_function: group()
for [i] in self -> func:
print: [i]
return "That's all, thanks."
func init:
print: (grouped_function: "LoveString""Mutable Amount of Arguments")
# LoveString
# Mutable Amount of Arguments
# That's all, thanks.
math()
会将传入的Value全部转成数字(失败时则转成0),并提供了一系列符合数学书写习惯的表达式:
root[math_instance] = math( 3^2 + 6%5 + root(4)by(2) + exp(ln2) )
func init:
print: root[math_instance]
# 14
注意:该高级优先行使用了不同的分词器,以尽可能符合正常数学表达式的书写习惯。
duo()
允许传入两个Value和一个高级运算符(“先计算再赋值”运算符,以及大小比较类运算符),Value会被转换为数字:
root[my_set] = duo( root[my_advanced_pass] = "5.0" )
root[my_set] = duo( root[my_set] += "1")
root[compare] = duo( root[my_set] > "5" )
func init:
print: root[my_set]root[compare]
# 6.0
# true
在循环结构中还会再介绍几种高级优先行。
匿名函数与“导向“算符
现在,我们掌握了了基于自定义关键字的方法
。教程的一开始我们也透露了用func:
关键字定义的匿名函数
,怎么把这两种元素统一起来呢?
现在我们可以使用“导向”算符->
帮助我们完成此操作。“导向”算符用于向匿名函数传参,并将具名方法等连接到匿名函数或优先行:
root[myfunc] = func: [a][b][c]
if ([d] in self) -> (print: [d])
print: [a][b][c]
if (not [c] in self) -> (print: "%n--String Lover)
func my_func: [d] -> root[myfunc]
func init:
print: (root[myfunc] -> "Love""String""Forever")
print:
print: (my_func: "I""love""string""forever.")
# Love
# String
# Forever
# --Srting Lover
#
# I
# love
# string
# forever.
通过“导向”连接具名方法和匿名函数时,具名方法的参数、匿名函数的参数都是内部字典的Key。这允许你将匿名函数导向具名方法时,要求具名方法传入更多的参数。
从示例中还可以挖掘到我们设计的语法糖:具名方法“多出来”的参数可以流入导向的匿名函数内。
注意:
没有匿名函数“导向”匿名函数的语法,“导向”的左侧若是匿名函数,则向匿名函数传参。
如果不导向具名方法的话,不装入优先行的匿名函数不会执行。
LoveString禁止了具名方法执行时导向参数,以保证具名方法的统一性和分词器设计的简洁性。
结构式
本节中,介绍程序设计中非常关键的选择结构及循环结构。
先从结构式开始:结构式是形如condition -> callable
的语句,callable
允许无参关键字、具名方法、匿名函数及优先行;为了降低语法复杂度,结构式自身不能向callable
传入自定义参数。但是,我们可以给出一个“结构式传参”的示例:
root[arg_to_structureline] = func: [a][b][c]
foo: [a][b]
bar: [c]
func init:
if (true) -> (root[arg_to_structureline] -> root[a]root[b]root[c])
选择结构的结构式是if () -> callable
。
强制要求用优先行(包含高级优先行)表示条件,以确保能捕获到返回值。
我们深知匿名函数是一种相当晦涩的设计,因此大多数情况只需使用如下的写法:
# 单行
func init:
root[input] = receive_and_pause:
if (root[input] == "hp+1") -> duo(root[hp] += 1)
else -> return
return_self
# 多行
func init: [sth]
if ([sth] == "") -> func:
[sth] = "String"
[lover] = "Lover"
print: ([sth] + " " + [lover])
# 该多行示例的输出结果(执行时不传参):String Lover
注意:结构式不自带字典,但匿名函数自带字典!
来到while
循环,我们提供了while_true () -> callable
和before () -> callable
两种结构式,见名知义。
for
家族则更加常用一些,我们提供了以下几种遍历模式:
# 字典遍历模式
for [i] in root -> func1:
for "sth" in root -> func2:
deepfor [i] in root -> func3:
deepfor "sth" in root -> func4:
# 高级优先行模式
for "Values" in range("0""100") -> print:
for "Values" in ftb("0""100""10") -> print:
可以看出for
家族的通式:for A in B -> callable
。
其中,A
如果是Key的话,会自动向“导向”的callable
传入一个Key;如果是Value的话,则传入一个键名字面量。
字典遍历模式中,B
一般是指向字典的元素,以root
和self
居多,也可以是自定义字典。
如果B
部分是空字典的话,该循环被跳过。
如果B
部分是一个Value的话,for
循环将会执行一次,此时传入的都是该Value。
对于高级优先行模式,range()
优先行要求内含两个数字,左闭右开,效果如该例:
func init:
for "Values" in range(0,2) -> print:
# 0
# 1
ftb()
是"from-to-by"的缩写,ftb([A][B][C])
表示从[A]开始(左闭),到[B]结束(右开),每次步进[C]个单位,效果如该例:
func init:
for "Values" in ftb("0""50""10) -> print:
# 0
# 10
# 20
# 30
# 40
deepfor
比较少用,但可以把传入的字典所包含的子字典一并遍历。高级表达式模式中,for
与deepfor
完全等价。
在LoveString中运用OOP思想
通过多层次的字典设计,可以实现面向对象的封装性。
通过编写“模板”,可以形成实例,也可以完成继承。例如:
root[Templates][Character] = {
[HP] = "100"
[MP] = "75"
}
root[Templates][Wizard] = (
root[Templates][Character] + {
[MP] = "200"
[Skill] = "Magic"
}
)
对于字典,
+
是合并运算符,深拷贝右侧字典,然后对左侧字典顺次赋值改键。“成员变量私有化“
子字典中加入
[lock] = "true"
,[lock] = "before_addup"
这样的键值对,则只能通过向上一级的匿名函数传参来访问该子字典。仅在Debug模式起作用。