Skip to content

jadedrip/simpleLang

Repository files navigation

SimpleLang 语言

A compiler for SimpleLang which is a modern language.

Compiler = Flex + Bison + llvm

目标 & 简介

SimpleLang 是拍脑袋的产物,试验性的产品,现在仅仅处于最初级阶段。随时会变化。 糅合了 go, Java, C++, Lua 等语言中我觉得“爽”的部分,作为个人兴趣爱好实现,不必太过期待。

它应该有以下特点:

  • 它应该可以和 c 较好的融合 也就是方便的调用 c 语言编译出来的库,并且用它编译的库,也可以被 c 语言较好的调用。 在后端采用 LLVM 的情况下,这个目标似乎不难实现。站在巨人的肩上,也可能让它更好的发展。

  • 它应该可以和 c++ 较好的融合

  • 语法上会比较接近 Java,抛弃所有 C/C++ 中的复杂部分,但引入一些高级特性。

  • 编译型语言,编译的代码应该有较高的效率。所有的类型都是“可决”(在编译期完全确定)的。

  • 简洁&优雅,表现力强,行尾不加 ;

  • 原生的支持委托(或者说函数对象?)和闭包

  • 原生的支持协程,多线程

  • 原生支持 utf-8, unicode

  • 支持国际化

  • 可选的 GC

  • 谨慎的支持操作符重载

  • 使用一个简化的异常系统来处理错误。

  • 支持模板

设计思想:

  • 尽量避免对象的复制,凡是复制,都需要明确使用 clone

基础语法

工程目录

SimpleLang 工程有固定的目录结构

[root]
	└ [doc]				文档目录
	└ [main]
		└ [resources]	资源文件目录
		└ [src]			源码目录,通过目录结构来定义包
			└ main.sc 	主入口文件(编译为可执行文件时有这个文件)
	└ [test]
		└ [src]			测试源码目录,
		└ [resources]	测试用的资源目录
	└ [subject]			子工程目录,除了没有 main.sc,其他和主工程一样,也可以在内部通过链接文件连接到其他目录
			└ 
	└ config.yml		配置文件

文件头部不需要写包名,通过目录结构来确定类的包名

SimpleLang 程序的目录结构是固定的,入口文件被必须为 main.sc,而一个程序可以引入多个包(或者叫模块),包一般被 7z 压缩为 spk 文件,包的目录同样是固定的。

命名

  • 变量使用小写开头,可包含字母和数字,驼峰格式,禁止使用符号和下划线
  • 类使用大写开头(内部类型除外),驼峰格式,禁止使用符号和下划线
  • 函数/方面名使用小写开头,驼峰格式,禁止使用符号和下划线

变量类型

支持的内置类型:

名称 符号 位宽
boolean 1 bit
byte 8 bit
char 16 bit
short 16 bit
int 32 bit
uint 32 bit
long 64 bit
ulong 64 bit
float 32 bit
double 64 bit

注:内部类型在函数中使用传值方式传递,其他类型传引用

其他类型:

类型 说明
class
interface
singleton 单例

语言级别支持的内部类

类型 说明
Tuple 元组
String 字符串
Array<?> 数组模板类,可以和数组无缝切换,特别的 Array 可以用原生字符串初始化。
Any 可变类型
Func 函数对象
Delay 延迟对象
Optional 可选对象
Delegate 委托(保留)
Proxy 代理(保留)

使用 const 来定义常量:

const pi=3.1415	// 注意:常量如果可以推断,可以不需要类型定义

以及枚举:

enum MyEnum{ red=0, blue=2 }	// 枚举和 int 可以无缝转换

枚举使用时为:

MyEnum.red

变量

SimpleLang 语言中除了内置类型(整数和浮点),其他类型都保存在指针中。
变量必须以小写字母开头,并且只能使用字母和数字,不能使用下划线。

设计语:通过将变量和类型使用大小写区分开,可以增强语法分析,避免歧义,禁用下划线是因为编译器会生成带下划线的函数,可以避免冲突。同时这也从语法层面,直接规范了风格,提高可读性。

定义变量可以用 var 来自动推导,用右值推断类型,甚至允许先定义,使用时再推导。

var i=10
var x = y = 100		
var a
if(..){
	a = 30.0f
}

不过变量的类型是确定不变的。因此以下代码是错误的

var name = "Hei"		// 这里 name 自动成为 String 类型
name = 15			    // 而这里会出错,因为 name 不能改变类型

另外,内部变量有初始值。

数组

数值类型(不是变量)可以定义成普通数组,数组有固定长度,并且不能更改长度,有个默认打开的编译选项,会让越界访问抛出异常。数组实际上为 Array 类型,(类型+[]) 只是简写,两者等价。 数组下标从0开始

int[] a=[ 0, 1 ]			// 数组可以直接初始化
Array<int> a = [ 0, 1 ]
int[10] array2				// 初始化一个空的数组  
int v=array2[1]     		// 数组取值

// 通过内置的 size 成员函数来获取数组的长度
var len = a.size()			// 注意,这个是编译期方法

当数组作为函数参数时:

func myFunc( int[] array ){ ... }

也可以定义多维数组

int[2, 2, 2] vec = [ [ [1,2], [1,2] ], [ [1,2], [1,2] ]  ] 
int v = vec[0, 1, 1]

另外单维数组也支持切片

int[] s=arr[startIndex:length]

需要注意的是,切片是引用,因此如果源改变了,切片同样会改。

注:如果想使用可变数组,可以使用 Vector 模板类。

对象数组

对象也可以定义为数组,但它有特殊性。

构造数组时,整个数组仅仅初始化内存空间,而不会构造内部的对象,并且空间是紧凑创建的,不会有对象头,内存会被置0,但不会调用构造函数。因此,你无法直接判定数组中的对象是否存在,是否已经被初始化。

而将对象放入数组,是一个复制操作,原对象和数组中的对象会脱钩,新对象的生存期完全跟随数组(除非手动从数组中删除),而从数组中取值,同样是一个复制概念的操作。当然,编译器会尝试分析生存期进行优化,如果变量没有逃逸,是不会有真正的复制操作的。

MyCls[2] myClss
var zero=myClss[0].val		// 对象还未构造,但 zero 的值是0,因为内存被置0了
if(myClss[0]) doSome		// 不能这么判断,表达式永远为真,编译会报错误
if(myClss[0].exists) doSome	 // 可以在 MyCls 里定义一个 boolean exists=true, 由于数组默认置空的,就可以用来判断对象是否存在了
myClss[0] = MyCls()		// 可以认为是 myClass[0] = clone(MyCls()), 当然,编译器会进行优化
var v=myClss[0]			// 原则上等于 v=clone(myClass[0])
v.val = 11			    // 数组内的对象不受影响
const x=myClss[0]		// 这样不会进行复制,x 被认为是 myClass[0] 的别名
x.val = 11				// 数组内的对象被改变

这么设计的目的,是极大的降低对象数组的开销,并把数组交给程序员完全管理,对象数组实际可以被看成一个内存池。对象数组很容易出错,因此使用需要谨慎,如果不追求性能,用容器类吧。

内部类 Any

Any类型可以保存任何值,并且保存类型信息。

Any 类型允许在运行期保存任何对象,可以判断它内部是什么类型。

Any i=20.0f    // 保存一个 float

然后你可以这样用

assert( i is Any )

when(i){   // 用 when 关键字进行运行期类型判断
	int : print(i)	// 在这个分支,i 被自动转换为 int 类型
	
	float : {
		// do float
	}
	default : {
		printf("unknown Type")
	}
}

if( i is int ){	// if 中如果有类型判断,块中会自动转换为相应的类型
	int b=i+1
	... 
}

Type type=i.type	// 获取类型(参见**反射**)
float x=i			// 或者 (float)i 强制获取 float 类型的值

但是,如果取值时,类型不一致,并且不能进行隐含转换,将抛出一个异常,这个过程是运行期的。

分支

if 采用 c 的语法, 增强 switch。

switch 语法如下,支持多种数据以及多种比较,只要是测试相等就可以。冒号后如果是多行,需要大括号。 并且不需要 case。

switch vegetable {
	"celery": vegetableComment = "Add some raisins and make ants on a log."
	"cucumber", "watercress": {
		vegetableComment = "That would make a good tea sandwich."
		comm = "多行需要大括号"
	}
	default: vegetableComment = "Everything tastes good in soup."
}

对数字也可以使用范围

switch intValue {
	1..10:		v="1 to 10"
	20..30:		v="20 to 30"
}

元组

SimpleLang 语言中支持元组,类似 C++ 中的 pair, tuple。Si 里的元组用圆括号括起来,必须从明确的值创建,并且创建后 不可修改 元组必须在创建时赋值,并且值不可更改。可以通过 := 来解构,或者使用[]取值,元组可以参与编译期运算。

var a=(1, "second", x)				// 直接创建
print( a[0] )						// 取第一个值(注:[] 操作是编译期的)
if( a[1] is int ) print("yes")		// 通过索引取类型

for( var i : a ){					// 通过 for 循环,在编译期解开元组
	print( i )
}

var n= (10, "Hello") :+ (20, "你好")  // + 可以拼接元组,结果为 (10, "Hello", 20, "你好")
var t=1 :+ (10, 20)					// + 也可以把元素加入元组成为一个新元组,这里等价于 var t = (1, 10, 20) 
PS: var x = you( myFun() :+ 10 )		 // 考虑到函数返回直接用 + 容易引起二意,还是用 :+,冒号代表操作元组

var b0, b1 := a				// 元组的自动解构,注意复制的是引用,变量数量可以比实际的少,但不能多。
int c0, float c1 := (10, 10.0)

var c=(0)			// 这不是元组,c是 int 类型

另外,元组的成员也可以命名:

var tuple=( key=1, value=2 )
print( tuple.key )

特别的,元组必须放在 = 右边或者接 -> 操作 ,也就是无根的元组是违法的。

( 10, 20 )		// 编译错误

循环

支持 while, for 循环,c 的语法。但不支持逗号多定义,多步进,不支持 do-while。 另外支持 for-each 形式

int[] arrays = [10, 20]
for( var i : arrays ){
	i = 30		// i是引用,这里将改变 arrays 中的内容
}

for( int i : 1..20 ){
	...
}

作为一个语法糖,在循环内使用 free 语句释放迭代器,编译器会尝试调用容器的 remove 方法(如果有),将对象从容器中移除。 remove 的返回值赋值给 i, 重新开始循环体 (continue)

for( var i : list ){
	free i		// 等价于 i=list.remove(i) continue
}

特别的,如果列表里保存的是元组(比如 map ),可以使用自动解构的语法

for( var k,v : map ){	// 同样,注意复制的是引用,因此改变 v 会改变 map 内的值
	free k
}

字符串

在 SimpleLang 里的字符串类是个内部类型,String,字符串类内部维护字符串编码。一般认为是 UCS-4 编码,表情符号占两个字符。

String eng="Hello world!"			
String str="中文"					
char aChar=str[0]					// a='中', 取出某个字符,为 UCS-4 编码
String str( bytes, Charsets.utf8) )	// 通过 bytes 指定编码来创建
str.bytes()				    		// 获取 byte,不指定编码,会默认返回 utf-8

另外,字符串也支持切片。

String s=str[0:1]	// 注意,这里不会复制内部字符串

连续多个字符串会拼接为一个

String s = "Hello " "World"

字符串通过操作符重载允许进行 + 操作,并且可以和整数、浮点数加,结果是字符串。

String s="" + 16		// 结果是字符串 "16"
s=R("Hello", "zhCN")	// 从资源文件获取字符串

双引号支持字符串模板,单引号不解析模板

String s="Value=$val and key=${cls.key}"  // 字符串模板
var x = 'No $Template'

注意:字符串内容不能更改。 如果需要可变字符串,使用另一个兼容类 StringBuffer。

操作符

支持大部分 C++ 操作符,但是不支持前置 --, ++。 支持 >>> 运算符(无符号右移)

函数定义

函数(方法)名必须使用使用小写字母开头,并且不能使用下划线。 函数这样定义

func first( int a ) : int {
	return a
}

调用时:

var x = first( 10 )

并且为了保证定义的清晰,如果省略返回值的定义,就是表示不返回。 参数允许可变参数,不过必须定义在函数的最后,也只允许一个可变参数.

另外允许多返回值,这时返回值会被包装为一个元组。 多返回值函数定义方式如下,返回值可以匿名也可以命名,命名的返回值在函数内可以直接作为变量操作.

func second( int a, var b, int ... args ) : int retval, int val {
	// 这里 args 被视为数组 array
	if( args.size > 0 )
		val = args[0]
	retval = a + b		// 直接操作返回值
	return	   // 可以省略变量列表
	int x = 12 // 编译错误,return 必须在块的最后 
}

注意,返回值如果命名,那么在函数开始,就会调用无参数构造函数构造返回值,如果没有无参数构造函数,编译会失败。

单行函数(语法糖)

func add(int a, int b) = a + b

单行函数使用推导获取返回值,必定有返回值,因此也不需要 return。

return

  • return 语句必须写在区块的最后,本区块后面不能有其他语句
  • 如果函数返回值都是命名的,那么可以不带后面的返回参数,特别的,没填充的变量会以默认值返回
  • 如果 return 后面带了返回参数,那么必须写全
  • 函数最后的 return 允许省略

可变参数可以是同一个类型的,或者使用 var 让它成为可变模板函数

func varFunc( int a, var ... args ){
	for( var i : args ){    // 这个 For 会在编译期被展开
		run(i)
	}
}

上面这个 for 循环会在编译时被展开成顺序的多个代码块。

func varFunc2( int a, Any ... args ){
	//  args 被视为 Any 数组
	for( var i : args ){    // 这个 For 会在运行期被展开
		run(i)
	}
}

函数可以使用返回元组的形式来返回多个变量,并且你可以用自动解构。

var first, second := fun( 10, 20 )

返回值如果有非命名参数,那么必须写在列表的前面

func second( int a, var b ) : int, float, int {
	return 0, 15.0, 2	
}

传参

SimpleLang 语言中,传入的参数如果是值类型(int 等数字),会被复制,而对象被视为引用,因此它的内容可以在函数中可以被更改。

比如:

String old="Hello"
cut2(old)
print(old)	// 输出 "Hell"

func third( int a ) : int {
	return a+1
}

参数中可以使用 var 关键字,这时候它同样被视为模板函数。 返回值也可以使用 var,这时通过 return 来推断返回值

func myFunc( var a, int b ) : var ret {
   return a+b   // 这时推断返回值为 int 类型
}

int x=myFunc( 10, 20 )

特别的,如果参数上加上了 clone 关键字,那么这个对象会被复制进入函数

func myFunc( clone int clonedValue ) {
	clonedValue.x = xxx	// 不影响原始变量
}

函数调用

函数使用比较宽松的调用形式,可以是顺序的,比如:

var first, second := fun( 10, 20 )

或者命名的形式:

var v = fun( name="myname", value=20 )

也可以2者组合

fun( 10, 20, name="myname" )

但是有个约束,包括,顺序形式的必须在函数调用的开头。并且所有的参数(除非有默认参数)都必须充满。

下面的写法是非法的(除非第一个参数就是 name):

var v= fun( name="myname", 10, 20 )

函数对象、匿名函数

SimpleLang 在语言级别支持函数对象、匿名函数,匿名函数不能是模板的,参数不能有默认值,不允许可变参数。

var add=func(int a, int b) : int{ return a+b }	// 匿名函数
int a=add(10, 20)								// 执行

如果没返回值,没有参数,都可以省略

var my1=func(int a){ ... }			// 无返回值
var my4={ return 20 }				// 语法糖: 无参数时 = 后面 func 可以省略, 返回值类型自动推导
func(int):float itIsAFunctor		// 明确的类型

同时,匿名函数可以引用外部变量(作为引用):

int myVal = 10
var x=func(int a){ return a+myVal }		// 注意这个 myVal 是引用,在匿名函数调用时取*当前*值

assert( x(20)==30 )

上面的代码演示了简单闭包,不过要注意的是,并行的情况下,myVal 可能被更改、互斥,这时候使用闭包需要特别小心。 另外,匿名函数里包含的是对象引用,因此如果在匿名函数里修改 myVal 的值,当匿名函数被调用时,myVal 就会被更改。 这点需要注意。

异常

SimpleLang 支持的异常。

/// 语言内部异常,所有异常的基类
class Exception{
	String message	// 异常信息字符串
}

// 定义一个异常
class IOException : Exception		// 定义一个新异常
class HttpException : IOException{		
	int code
}

try{
	var a=func( 10 )
}catch( HttpException IOException e ){	// catch 允许多个异常类型
	print( e.message )
}finally{
	// 最后会执行的代码
}

// 简化的异常处理格式,对函数的异常直接处理,需要注意的是,函数后面的 catch 只能有一个
var i=func( 10 ) catch(e) {
	print(it)  // it 是 Exception 类型
}

func(10) catch {}	//明确忽略异常

如果一个异常未被捕获,会转去 公共异常处理函数,对于线程/协程 将打印日志,然后结束线程/协程, 如果主线程被关闭,那么程序将退出。

一个函数也可以使用 nothrow 关键字明确声明自身不抛出异常

func nothrow willNotThrow() : int 

注:一个 nothrow 的函数,编译器会尽量检查异常情况,如果调用了带异常函数而没捕获,将出现编译错误。 而如果 nothrow 的函数内抛出了空指针之类的未知异常,将无法捕获,而直接转去 公共异常处理函数

空指针

变量可以通过 null 来初始化。

String k = null 		
assert (k is null)
k = "10""
k = null
k = "20"

某个默认打开的编译参数可以在运行时让空指针抛出 NullPointerException 异常,当然,这会略微的降低执行效率。 ?: 操作符可以在指针值为空时提供默认值

var val = getValue() ?: defaultValue

基础

每个文件(.sc)可以定义一个或多个类。 访问控制被简化,si 中的类类似 Java。成员变量、方法只有公开包内的,公开的可以被包外访问, 否则只允许同名包或者子包 或者继承的类来访问。

包 org.first.second 是 org.first 的子包

纯数据类

单纯保存数据用,类似 c 里的 struct。不具有成员函数(也没有的 get/set)。纯数据类构造函数默认生成,而不能自定义,构造函数按顺序填写字段,并且默认空值。

data MyData{
	int val;
	String str;
	
	// 默认生成
	init(int val=0, string str=null);
}

MyData x(10, "你好")

纯数据类可以继承,并被一般类继承,但不能从一般类继承。

一般类

代码开始

/* File MyCls.sc 开始 *
class MyCls {	// 定义类
	// 语法糖在变量前加个点,会自动赋值到内部变量
	init(int .privateValue = 20 ){}	// 变量前加 . 等同 this.privateValue = privateValue

	finalize{           // 析构函数始终是无参数的,不需要括号
	}

	clone{              // 克隆函数
		return MyCls(){
			it.key = clone(this.key)
			it.value = clone(this.value)
		}
	}

	int key = 1			// 创建时初始化(先于构造函数)
	int value			// 创建时默认为0
	
	func do_something(){
		this.key++  // this 是自己
	}
protected:	// 类中有且仅能有一个 protected 分割线,分割线上部的是公开的变量、方法,下部是
			// 保护的,只有在同一个类、子类或继承类内可以访问
	int privateValue	// 内置类型初始化为 0
}

注:成员变量的默认值必须能确定,不能是模板的,也不能是成员函数调用或引用另一个成员变量(这时类还没构造)

类里没有静态变量或方法,请用函数、全局变量来替代。

设计语:使用 Type name 这样的方式构造,可以帮助 IDE 自动补全(输入 My, IDE可以帮你连变量名一起补全了)

继承&重载

继承和重载的概念被简化,类可以单一继承,可以有多个接口实现。但类不再有虚函数,改用函数对象替代。

class Base{
	func cantOverload(){	// 普通函数不可以重载

	}
	
	func(int) virtualFunction = null 	// 代替纯虚函数
	func(int) canOverload = func(int v) {
		doSomethine()
	}

	var myFunc = func(int v){		// 函数对象
		print(v)
		virtualFunction(v)	// 调用虚函数
	}
	
	const func(int) constVirtualFunction = null 	// 真纯虚函数
}

// 类可以继承,只能单继承,但可以有多个接口
class Second : Base, Interface0, Interface1 {        
	func cantOverload(){	  // 这会是个全新函数,编译器报警

	}

	virtualFunction = func(int v){	// 重载虚函数
		print(v)
	}

	myFunc = func(int v){
		Base:canOverload(v)		// 强制调用基类函数
	}
	
	constVirtualFunction = func(int v) {
	
	}
}

Interface0 obj = Second()
obj.virtualFunction = xxx // 还能改
obj.constVirtualFunction = xxx // 编译错误,不能更改

相应的,类没有虚类或者纯虚类,所有类都可以被构造,如果有些类的函数没实现,就是一个空的函数指针,可以通过后期赋值的方式来实现。如果要防止它被构造,可以把构造函数放入 protected 区域。“虚函数”前可以添加 const 关键字,有 const 关键字的变量构造后就不能再更改了,因此编译器对这种变量可以优化实现。

Get & Set

SimpleLang 支持成员变量的 Get & Set,set 方法在变量设置时被调用,而不会预先设置值。

class MyCls {
	int valueA;
    
    get valueA {	
        return valueA
    } 
    set valueA(int newValue){  
	    valueA = newValue
    }
    
    get fakeValue: int {		// 可以 let x = myCls.fakeValue 这样调用
    	return valueA + 10
    }
	
	get virtualValue = func(): int {	// 可以 let x = myCls.virtualValue 这样使用
									 //   另外可以 get(myCls.virtualValue) = func() -> int {}	这样“重载”
	}
	
	set virtualValue = func(int newValue) {
	
	}
}

class YouCls : MyCls {
	get(virtualValue) = ...	// 继承内重写
	int b
}

YouCls you
print(you.valueA)	// 返回 b值

MyCls cls
get(cls.virtualValue)=func(): int{ // 外部重写 set 方法

}


类的构造

对象构造使用构造函数的形式,括号不可省略。

MyCls my              	// 括号可省略
var a=MyCls(10, 20)		// 通过构造函数构造,参数表使用逗号分割,当然可以 var 推断
List<String> arr= ("Hello", "World")	// 通过元组初始化列表

MyCls c(10){     		// 小技巧,在构造时,后面直接接大括号,在构造后,直接执行语句块
	key=0				// 在构造函数执行后执行
	val=20
}

对象不构造时,必须赋null值:

MyCls a = null

对象的传递,都是浅copy,除非明确指明复制

MyCls a=clone(b)	// clone 会调用复制函数进行深 copy。

对象可以被理解为都保存在智能指针中, 指针的赋值需要使用等号。

MyCls x=null
if( x==null ){			// 可空对象和 null 的比较,会判断对象是否为空
	x=Cs()          	// 这里会改变 x 指向的对象
}

Cs a
Cs b = a    // b 指向 a
b.val = 1   // 这里同时会改变 a 指向的对象值

另一个指针专用操作符是 ?:,在指针为空时返回对象(语法糖)

int a = nullable() ?: defaultValue		// 为空时 a 赋值为默认值

设计语:操作符重载有可能引起歧义,需要谨慎思考

类外部方法增强

通过在类外部定义额外方法,可以增强类

fun MyCls.other(){	// 本函数定义在外部,通过 import org.other 引入
	this.val++
}

MyCls cls()
cls.other()			// 可以像内部方法一样使用

循环解偶

两个类互相引用对方的情况是被禁止的(即使是间接引用)。但可以定义子类。 子类只允许在类内被构造,并且可以访问父类的变量。

设计语:循环引用本身是一种并不太合理的设计,干脆从语法上禁止了

class MyCls3{
	int var=0
	class SubCls{
		int subVar=2

		func incVar(){
			var+=subVar // 子类可以访问父类的成员变量
			super.print()  // super 是父类的指针
		}
	}

	SubCls cls=SubCls()

	func print(){
		Console.print(cls.subVar)
	}
}

垃圾收集

对象分托管和非托管两种,托管的对象会由 GC 管理,而非托管对象必须手动释放。但不管托管或者非托管对象,都可以有析构函数, 托管对象的析构函数仅手工调用或触发 GC 时才会被调用。

对象默认是托管的,非托管对象一般人不推荐使用。

var managed = ManagedClass() 	// 托管的
free(managed)					// 调用析构函数

var unsafe = new Unsafe()
free(unsafe)			// 析构

语法糖:对象作用区

通过在对象后直接挂接代码块,可以以这个对象为“基准”来执行代码。区块中的所有函数、变量会先在这个 对象内部查找。 另外,代码块中默认使用 it 代表本对象。
注意,代码块仅仅在对象不为 null 的时候才被执行

var i = a {		// if(a!=null) 才执行
	doSomething()
	it.name = "hello"	  // it 关键字代替本身
}

这相当于:

if(a != null)){
	a.doSomething()
	a.name = "hello"
}
var i=a

设计语:块还是限定为禁止返回,否则最后一个空荡荡的变量容易引起混淆

某些时候,为了防止多重嵌套代码块中 it 冲突,你可以通过 it 重命名来给 it 指针命名:

class MyCla{
	fun my(){
		a { other :
			other.hello()	
		}
	}
}

显式类型转换

除了默认类型转换,还可以使用下面的显式转换,如果后面接操作符,显式转换优先进行.

var a=(MyClass)b			// 尝试将 b 转换为 MyClass 类型
(MyClass)b.funcInMyClass()	// 先转换在调用
(MyClass)b{					// 转换再调用对象作用区
	funcInMyclass()
}

但显式转换也是有限制的,比如你并不能把 int 转为一个不兼容对象。显式类型转换仅能用于基类转换,等同 c++ 的 dynamic_cast

单例

单例受到语言级别的支持。它的声明类似 class, 仅仅是把关键字 class 改为 singleton。

////// 文件开始 ////////
singleton MySingleton{	// 单例的公开定义部分
	func astCallit(){}
}

当编译器发现包被引用,就会在程序启动时线程安全的初始化所有包内的单例,并且到程序关闭时才被析构。

import org.MySingleton	// 引用就初始化

模板

注意:凡是涉及模板的类、函数,都必须通过源码的形式存在导出包里。

模板类

可以通过把类中的成员函数,定义为 var 来创建模板类,模板类必须在构造时,能确定所有模板成员的类型。

class MyTuple {
	var left
	var right
	init( var .left, var .right ){}		
}

MyTuple<int, int> v = MyTuple(10,20)	// 通过构造函数确定所有类型

或者和 Java 类似,用命名的类型占位符来定义模板类

class MyMap<KEY, VAL>{	// 注意:类型占位符的约束是必须全大写
	KEY key
	VAL val

	init(String name){	// 
  	}

	func templateFunc( var a ){		// 函数直接使用 var 来定义模板函数
		if( a is int ){		        // 这里的 is 是编译期的操作符,因此这里的 if 也是编译期的
			a++
		}

		def A = b
		A b							// b 定义为 a 相同的类型

		Type c=typeof(a)			// Type 是种描述类型的特殊类型
		when( a ){					// 用 when 判断类型
			int : {

			}
			float : {

			}
			default : {

			}
		}
	}
}

var my = MyMap<String,int>("Hello")		// 构造时必须可以推导类中所有类型

模板函数/成员函数

当参数使用 var 或者使用类型占位符来定义时,这个 函数/成员函数 就是一个模板函数

func templateFunc( var a, int b ) {
	...
}

对于模板函数,参数类型确定了以后,返回值类型也就被确定了。

如果你希望返回值的类型在函数中来推导,那么需要使用 = 来连接函数定义和函数体

func<T> templateFunc( T a, T b ) = {
	return a+b
}

内部函数 typeof

虽然 typeof 看起来像函数,但它其实是在编译时起作用,用来推导类型

类型定义 def

可以使用 def 来为复杂类型,取一个别名

def AFun = func(int, int):int
def MapClass = MyMap<int, String>		// 定义模板类

var m = MyMap( 10, "Hi" )
def IntMap = m						// 可以理解为 def IntMap = typeof(m) 的语法糖

需要注意的是,def 定义的类型是仅仅是原类型的别名。

常量推导

如果常量传入模板函数,那么函数在编译期,就会被计算并展开

func myTemp( const a ) : var c{ // 通过 const 关键字保证只有常量可以传入
	if( a == 1 ){
		return a+10
	}else if(a=="Hello"){	// 传入 常量整数时,这分支都不会被编译
		return "World"
	}
}

myTemp( 10 )	// 在编译时不会生成函数,直接替换为 a+10

var x = 10
myTemp( x )		// 编译错误,禁止传入变量

const y = "Hello"
myTems( y )			// 传入的是常量,按模板编译

类型操作符

SimpleLang 将支持一些类型操作符,结合模板,可以实现编译期编程。

操作符 is

在编译期判断类型可以使用 is,语法是

A is B

A 允许是变量或者模板变量 结果将是 1 或 0。A 可以是变量,或者模板参数,B 为类型或接口。需要注意的是,这里并非要求 A,B 是完全一致的类型,当 B 是从 A 继承,或A实现了接口B,也将返回 1。 但相对的,如果 A 和 B 都是模板类型,那么模板参数不一致(即使他们有继承关系),那也返回 0

操作符 ===

在编译期判断类型是否完全相等

A === B

A 可以是变量或者模板变量,B 必须为类型或接口,和 is 相近,但 === 要求类型完全相等。

操作符 if

当 if 后的括号内是一个类型,那么它就成为一个编译期的类型操作符,仅当条件为 0 时,编译 else 语句块, 其他任何类型都编译 then 语句块。

if(a is int){		// 当 a 类型为 int 时被编译
	int b = a+10	// 这里 a 会自动改为 int 类型,不需要强制转换代码
}else{
	a = "Hello"	// 虽然类型错误,但这里没编译,因此不会报错
}

操作符 `(反引号)

反引号将尝试从类型中按名称取出成员,如果成员不存在返回 0,存在返回成员的类型。 这可以在编译期判断类是否包含某个成员。

 if( a`name` ) ...
 if( a`myfunc` is func(int, String):String ) ...

操作符 for

for 可以用来解开通过 ... 传入的多个参数等,也可以解开元组,这个过程是编译期的。

func my( var ... attrs ){
	for( var i: attrs ){
		// 这里 i 的类型会按输入变化
	}

	var x=( 10, 20, "Hello")
	for( var i: x ){
		print i
	}
}

操作符 ==,!=,||,&&

当两个常量进行布尔运算,他们也会在编译期运算为 1 或 0。

var a=(1==1)		// 1

模板推导函数

由于函数的返回值可以通过 return 来推导,因此可以通过写一个完全静态的函数,来实现计算类型的模板函数。

func TplFunc( var a, var b ) = {
	if( a is int && b is int)
		return 1		// 真类型,if( TRUE ) 永远真,并且在编译期就处理
	else
		return 0
}

var a = 10, b=10
if( TplFunc(a,b) ){	// 静态语句,在编译期展开
	int c = 10
}

接口

SimpleLang 可以通过 interface 关键字定义接口,是一个抽象类型,是抽象方法的集合,接口所有的方法、变量都是公开的。

接口必须以源码的形式导出。

接口可以带默认方法、变量默认值,但需要注意的是,接口的方法会被认为是函数对象,接口内如果包含模板变量、方法,它只能被用在使用源码导出的函数里,事实上,如果函数体是使用源码导出的,编译器会按模板函数的方式来编译带接口的函数。

比如

interface MyInterface{
	int value
	func getSome():int	// 这是个函数定义
}
func notTemplateFunc( MyInterface inc ) // 函数体通过二进制(c函数)方式导出 	

有且只有几个简单条件的接口,可以通过尖括号在参数中直接定义(匿名模板接口)

void aFunc( <int a, String b> inc ){ // 传入对象必须有个int型成员变量a, 一个 String型变量b
	inc.a++
}

接口无法被实例化,但是可以被实现。实现接口的类会生成接口定义的成员变量和函数指针。

class MyClass : Base, MyInterface{	   
}

MyInterface i=MyClass()		// i 被认为是 MyClass 的基类

void bFunc(MyInterface v){  // 必须从 MyInterface 继承的对象,才能传入
                            
}

或者,接口也可以隐式实现:

class MyClass {
	int value;
	func getSome(): int {
	
	}
}	
// 虽然 MyClass 并没有显式实现 MyInterface,但它包含了 MyInterface 所需要的所有元素,因此也可以直接传递给接受 MyInterface 作为参数的函数。
func myFunc( MyInterface a ){} 
MyClass x
myFunc(x)	

另外,和 Go 不同,纯空的接口是非法的。

语言特性

Optional 对象

Optional 可以保存一个内部对象,可以通过

Optional<int> opt=12
opt.isPresent()	// 是否存在

opt->(int v){ // 当值存在时被调用

}

int? optInt	// 其实是  Optional<int> 的简化写法

线程

创建线程由线程库支持,类似 Java

Thread thread(daemon=true, level=3, loop=false) // loop = true 时线程函数会反复执行,直到线程收到中断信号
thread->{       // 运行线程
	aFunc()
}
thread.cancel()		// 尝试发送信号关闭线程
thread.join()	 	// 等待结束

如果需要锁,代码如下

Mutex myMutex=Mutex()			// 定义一个锁
myMutex -> {
	// 锁内
}													// 这里解锁

原子操作由库来支持:

var a=Atomic(10)	// 原子的 int
int v=a++           // 原子的 +1 并返回新值
int cur=a.compareAndSet( 11, newValue )

另外,如果库是二进制实现(无引用计数),那么可以在函数参数上添加 $ 符号,强制引用, 由库内来释放它(仅仅在库内处理有多线程等逃逸的情况下有必要),这样编译器在调用函数时, 不会额外增加一次引用计数。 当然,未来二进制包的函数定义是由编译器来生成的,因此一般情况下用不到。

func myLibFunc( $MyObject obj ) // 这说明 obj 在函数内会逃逸

异步协程

同时,Si 包含一个小的语言库,支持协程,并且尽可能自动维护,这里的协程近似 Go 语言的协程,是异步执行的。 在一个函数、区块调用前加上 go 关键字,这次调用就会在一个新的协程中并发执行。 当被调用的函数返回时,这个协程也自动结束。

go {	// 通过go来创建一个并行任务
	print( "go" )
	yield();  // 放弃 cpu
	sleep(x); // 放弃 cpu 并等待最少 x 毫秒后继续
}

go dosomthing()		// 并行执行函数

另外 go 的返回值是一个协程对象,可以有限度的操纵协程。

func doSomthing() : int 

Coroutine<int> c1=go doSomethine()
Coroutine<int> c2=go doSomethine()
....
var v1= c1.await(4000)  // 等待协程执行完毕,最少 4000 毫秒后还未结束将抛异常
var v2= c2.await()

SimpleLang 通过通道来支持跨协程数据交换,成员函数 await 可以把异步操作写得更像同步操作。

var chan=Chan<int>()	// 实例化一个通道

go {				 // 并行执行语句块
	int i = chan.await(4000) // 这里会阻塞,直到 4000毫秒后超时抛出 TimeoutException 异常,或者 chan 被其他线程调用,参数会作为 await 的返回值返回。
		catch(TimeoutException e){

		}		 

	// 后续的代码
	print(i)	// 输出 10
}
chan(10)	// 调用通道,把数据塞进去,这里会阻塞,一直到数据被取走

var cachedChan=CachedChan<int>(10) // 带缓存的通道,缓存满前不会阻塞

特别的,如果一个协程被阻塞,它可能会被调度去干别的事,因此唤醒可能并非“及时”的。

包 & import

SimpleLang 通过 import 导入包,import 只能写在文件头部,简单起见,SimpleLang 总是一次导入包里首层所有的变量、类定义、函数等,而其他层不能在包外访问。

import org.simplelang

如果包有冲突,那么可以创建包别名

import org.simplelang as sl

sl:MyClass my

也可以用全名来使用包内函数或对象而不需要导入

org.simplelang:printLine("Hello")

包只有根目录下函数、接口、类才能被其他模块访问。

org.simplelang.inner:StringImp imp	// 编译错误,无法访问

反射

SimpleLang 支持静态反射

func myTemplateFunc( var param ){
	for( var i,name of param ){	// 枚举 param 的成员变量、成员函数,它会被展开为代码,name 是它的名称,是个静态字符串,由编译器支持
		if("open"==name){	// 如果这个字段叫 open
		
		}
		if(name.startWith("my")){ // 
		
		}
		
		if(i is int){
		
		}else if(i is func){	// 如果是成员函数
		
		}else if(i is func(int, int)){
		
		}
	}
}

注意,由于是静态反射,因此模板函数里的代码必须是静态的,另外特别设计静态字符串,支持有限的几个方法。

其他

注解

SimpleLang 支持注解,未来在插件里支持注解的使用

class MyClass{
	@ReflectName( name="value", idx=12 )	// 使用注解对象来进行注解
	int a 

	void doFun(){
	}
}

C 对象定义

@Clang("my_c_object")
class MyCObject
{
	@Clang("my_obj_add" )
    func at( int idx ) : int  // int my_obj_add( my_c_object* me, int idx )

    func size() : int  // int my_c_object_size( my_c_object* me )
}

和其他语言协作

可以引入 c 或其他语言写的库会被编译成包,以便被 SimpleLang 调用。Si 语言可以输出标准 c 函数,以便被其他语言调用。 lib 文件、需要对应的头文件及适配文件等需要的东西,会被某个处理软件打成一个包,并放在编译程序能找到的地方。 而 SimpleLang 可以编译成 c lib, 函数会按约定转换成 c 的格式。

操作符重载

SimpleLang 支持有限的操作符重载,可以对类重载一元或二元操作符,但不支持二元操作符在前方的重载

class MyClass{
	operator + ( int right ){	// 二元操作符的函数重载(默认的返回 MyClass 类型)
		return this
	}

	operator ++{					// 自增在后
		// return this 可省略
	}
}

备选(思考中,暂时不实现)

延迟优化( Delay 类型)

SimpleLang 的函数参数,允许使用延迟生成的技术以优化效率。它让参数仅在被首次使用的时候,才会被生成它。 比如:

trans_data( get_data(), x)

而 trans_data 的代码如下:

func trans_data( var v, bool x ){
	if(x) print(v)
}

这段代码里,v 通过 get_data() 获取值,但在 trans_data 中,如果 x=false,v根本不会被使用。这个时候 get_data() 的调用 是完全没有必要的。而通过延迟生成技术,只有在v参数实际被使用时才会尝试“构造”它,因此,如果 x=false,get_data() 会被直接 放弃,生成的代码类似下面的

void trans_data( bool x ){
	if(x) print( get_data() )
}

要启用延迟优化,调用代码不用做任何更改,只要函数是接受 Delay<?> 类型的参数。

trans_data( get_data(), x)		// trans_data 的第一个参数必须是 Delay<?> 类型

func<T> trans_data( Delay<T> v, bool x ){
	if(x) print(v)
}

其实也不一定用在函数里

Delay<int> x={ get_data() }		// 这时 get_date() 其实没有被执行
var k=x							// 这时才会执行,并且只会执行一次
var k2=x						// 注意,这里不会重复执行

后置函数调用

函数另一种调用方法是通过 :: 后置

10 :: fun 	// 等价于 func(10)

元组也支持 :: 操作

(10, 20) :: myFunc 	// 等价于 myFunc(10, 20)
let tuple=(10, 20)
tuple :: myFunc		// 等价于 myFunc(10,  20)
let v = ((10, 20) :: firstFunc) :+ 30 :: second // 等价于 let v = second( firstFunc(10, 20), 30 )

如果你想玩一下函数式编程,这种语法也许会让你的程序更容易阅读

非托管对象

某些类可以用 class* 定义为非托管类,托管类在构造时不使用 GC 以提高性能,并且必须手工 free。

class *UnmanagedClass{
}

var unmanaged=UnmanagedClass()
free(unmanaged)

非托管类如果构造托管类,也将使用非托管的方式构造(注:这里可能有危险,谨慎使用)

对象追踪

class Connect

class MyObject

var conn = Connect()
var my = MyObject() link conn
var myRef = (my:conn)

my 对象的生存期将会跟随输入的对象 conn,成为 Connect 的子类。

编译器插件

在代码中可以嵌入由插件程序解析的代码,插件程序会把代码翻译为 SimpleLang 语言。

var json = ```json```
  { im : " }
```

这段代码会调用一个叫json的插件,把中间的代码解析为 SimpleLang 语言的代码。

接口代理

接口代理是接口的另一种使用方法,会以名称的形式来调用接口内的函数

interface MyProxy{
	func run(String data)
}

class MyProxyImp {
	func invoke(String methodName, Any[] args) : Any
}

MyProxyImp obj()

MyProxy imp = proxy<MyProxy>(obj)	// proxy 是内部函数
imp.run("Hello")	// 

编译期反射

class A
A a

for( def i : A ){
	a[i] = xx // 赋值
	if( i.name == "hi" ){ // 编译期比较

	}
	i.annotations["CLang"] {
		var n = it["name"]  
	} 
	def T = i
}

国际化

通过翻译表来实现国际化

print( "Hello %t World" )

翻译文件: lang.zhCN.ini

[zhCN] "Hello %t World"="你好 %t 世界"

通过 Lang.set("zhCN") 切换文本

About

A compiler for SimpleLang which is a new language.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published