v0.2现在只是v2.0。这种版本控制方式将与VS Code市场的方式更好地配合使用,即< major>。< minor>。< patch>。我想保留将来升级次要版本的选项。如果0.2冻结了主要和次要字段,那将无法正常工作。
接下来是文档。实际上,该语言的测试阶段应该花更长的时间,但我决定加快发布时间。这样更好。我不会尝试强制测试,而是会在进行过程中有机地发现并解决错误。编写文档也将迫使我深入介绍该语言的功能。
我尚未校对文档,但已将我想要的所有内容都放入其中。特别是,串行器是Spiral功能的良好且重要的展示。当前格式的文档的一个弱点是它不会对初学者友好。我将根据反馈意见决定如何处理。如有任何疑问,请随时在问题页面中提问。
达到4.1k行时,它的数量不会令我感到尴尬。按住向下翻页键时,花一些时间才能到达底部。我议程上的下一件事情是让Spiral在Linux上工作。 .NET现在应该是可移植的,但是谁知道移动操作系统时会破坏什么。
之后,我将确保将悬停注释添加到该语言中。之后是对文档进行校对。
现在,Spiral可以轻松携带,并且我刚刚添加了悬停注释。我在这里添加了语言功能。下一步是校对这些文档。
当世界无情地奔向明天的黑胶w时,需要面对它的力量。
在整个编程语言的历史中,选择是在快速还是表达之间进行选择。 C和Lisp系列语言明确了这两个传统。为此已经付出了很多努力,但是随着语言的发展和前进,它们总是远离裸机,从而失去了性能所需的部分核心活力。
造成这种情况的罪魁祸首是数十年前Lisp引入的默认分配法则。对于弱类型系统的语言来说,这是一个拐杖。
通过堆分配进行抽象是一个死胡同。在当前仍是CPU仍是计算的主要驱动器的现代计算机上,它运行得很好。
它不适用于GPU之类的设备,其余部分则无法使用。未来许多最重要的计算设备将不支持堆分配,因此需要一种替代方法来发挥其全部功能。至关重要的是,用于该任务的语言必须对内联进行出色的控制。因此,内联必须作为语言的保证,并且是类型系统的一部分。
内联是一种折衷方案,表示用于计算的内存交换。它应该是默认值,而不是堆分配。
足够善于传播信息以便能够表达内联保证的语言也足够强大,可以很好地表达许多其他内容-无需任何抽象开销。
螺旋类型是静态类型,具有轻量级,功能强大的类型系统,具有动态语言的表现力和C的速度,它是阶段函数编程的结晶。它具有内涵多态性和一流的分期。其主要目的是为新型AI硬件创建ML库。
该语言在VS Code市场上发布。只需安装“螺旋语言”插件即可。这将同时安装VS Code编辑器插件和编译器本身。编译器本身需要.NET Core 3.1 SDK,并且可以跨平台移植。语言服务器使用TCP与编辑器进行通信,因此允许其进入防火墙。
Alpha-该语言需要更多测试才能被认为是可靠的。目前只有F#后端。假设我可以得到新颖硬件的赞助商,将会增加更多内容。如果您是有兴趣赞助Spiral的AI硬件公司,请与我们联系。
除了缺少后端和库之外,它还具有基本的软件包管理功能,并且一些重要的部分评估器优化可用于改善大型代码库的编译时间,这些改进已留待以后使用,直到需要它们为止。但是总体而言,甚至只是考虑编译时间,这种情况也比v0.09时代要好得多。
Spiral程序的层次结构是程序包的图形,程序包的内部有一系列的模块,内部有一系列的顶级语句(例如类型定义和函数),内部有一系列自己的局部语句。
可以解析螺旋文件(.spi或.spir)而不依赖于任何其他文件,但是为了使类型推断起作用,它们必须具有所有者包。spiproj(名称和后缀都必须匹配)文件并成为序列的一部分。当从.spi / .spir文件所在的文件夹向外搜索时,找到它们的第一个文件是其所有者。另一方面,package.spiproj文件只能引用其拥有的文件-如果任何子目录都有另一个包文件,那么将是一个错误。
启动Spiral项目的方法是在某个文件夹中创建一个空的package.spiproj文件。
现在,如果该文件夹没有a.spi,您应该在编辑器中看到一条错误消息,指出该错误程度。项目文件是交互式的-无需在编辑器的树浏览器中创建文件,而是可以将光标放在上并选择代码操作“创建文件”。为了实际创建它。存在的文件和软件包在软件包文件中还将具有指向它们的链接。
创建时a和b-分别是a.spi和b.spi。但是b-具有特殊的行为,因为它将模块内联到封闭范围内。尽管a中的所有内容都将其文件名作为其模块名,但是b的语句将直接包含在封闭的语句中。
创建时c *和d *-分别是c.spir和d.spir。与b-类似,d *-中的-用作包含后缀。 .spi和.spir文件在处理方面有重要区别,这将在后面的部分中介绍-现在,文档将涵盖常规的自上而下的.spi模块。
软件包文件中模块字段的解析是缩进敏感的,因此不会被视为some_folder文件夹的一部分。
您可以使用代码操作从打包文件中删除和重命名文件和文件夹。重命名将更改磁盘上的文件或文件夹名称,但实际上不会重命名对此文件的引用。
假设您有一个包含子文件夹a,b和c的文件夹,每个子文件夹都有自己的package.spiproj文件。如果要c引用a和b,则应该这样做。
软件包还支持包括后缀-。例如,这对于包括核心库很有用(假设它在目录中)。
默认情况下,模块目录是当前目录,而软件包目录是父目录,但是可以显式设置它们。
而不是在包文件的父文件夹中查找包,而是使用|。一元运算符指示编译器在其自己的可执行文件的父文件夹中查找-换句话说,插件文件夹本身。这是将核心库与插件捆绑在一起的便利。
除了这4个文件包之外,软件包文件模式还支持名称和版本字段,但这些文件目前不以任何方式影响编译。
包的妙处在于它们的处理是同时完成的。尽管像F#中一样严格按顺序处理模块,但包更灵活。包之间的循环链接虽然不允许,但会报告错误。
只要任何已加载的软件包有错误,键入推断就不会起作用-更改将被缓存,直到解决了软件包错误并仅显示以前的结果。
螺旋语言分为自上而下(.spi)和自下而上(.spir)段。两者之间的区别在于,自上而下实际上具有基于统一的ML样式的类型系统,而自下而上则通过部分评估进行类型传播。
这是Spiral v2的一项重大创新。在其先前版本中,Spiral没有自上而下的细分。
我花了一年时间进行这样的编程,但过了一会儿我就厌倦了。一年后,我开始意识到表现力和力量,尽管本身并没有有价值和必要的目标。我爱上了一段时间,因为它是编程和静态类型的新视角,但最后我才知道真相-仅仅因为部分评估者可以做任何事情,并不意味着它应该做所有事情。
自下而上具有较高的表现力和功能,但是即使不需要这种功能,用户也要为此付出代价,因此在其上放置较弱但更易于使用的类型系统具有很大的好处。我希望至少有95%的时间在Spiral中进行编程,这需要借助自上而下的类型系统。自上而下的细分是Spiral的简单组成部分。
在接下来的部分中,我将使用带有核心库和任意模块的package.spiproj文件来写入内容。
编译模块的方法是在编辑器中将其打开,然后从命令面板中使用Spiral:Build File。在VS Code中,最简单的方法是按F1。默认情况下,它也按Ctrl + Shift + P键。
使用build命令将部分评估main函数。这将创建a.fsx,并将以下残余程序作为输出。
现在,主函数将单元类型作为参数。将来,这种情况可能会发生变化,因此它需要一个字符串数组,此外还有更多可用的编译选项-我希望Spiral可以用于编译库,而不仅仅是编译main。但是目前,状态为早期字母,并且该语言仍处于测试阶段。
在最高层,Spiral非常类似于F#和OML等各种ML系列语言。毫无疑问,它是一种急切且不纯洁的静态类型函数语言。因此,您使用这些语言掌握的任何技能都可以直接转移到Spiral。特别是F#具有许多学习资源,这些资源的广度对于我而言无法涵盖在本文档中,因此我特别推荐给初学者。本文档适用于那些已经精通功能语言的人以及知道如何编程并希望将其技能提升到更高水平的人们。
讨论Spiral的设计注意事项以及为什么按原样进行设计。
特别是当我加入要点和功能时,对于初学者来说,这些材料将变得很难理解。 下一部分应该很简单。 inl main()= inl x = 1 inl y = 2 x + y 如果您尝试编译以上内容,您实际上会得到一个错误,即main函数不应该是永久的。 如果将鼠标指针悬停在主网上,您会看到其类型为“全部” {number}。 ()-> ' a。 这里'是具有数字约束的类型变量。 ()是单位类型,而()-> ' a表示从单位到a的函数。 螺旋最类似于F#的设计,这是第一个区别。 像Haskell一样,它支持多态数字文字。 实际上,如果将鼠标悬停在1或2上,您会看到它们的类型为' a。 为了编译此段,需要做的是使字面量具体。 inl main()= inl x = 1i32 inl y = 2 x + y inl main()= inl x = 1f64 inl y = 2 x + y
将常量更改为浮点数很容易。一种替代方法是只在某处提供类型注释。
inl main():u16 = inl x = 1 inl y = 2 x + y
现在,将文字推断为16位无符号int。这里还有一个...
inl main()= inl x = 1:i64 inl y = 2 x + y
现在,这里的文字现在是一个64位带符号的int。 Spiral中的原始数字类型包括带符号的整数(i8,i16,i32,i64),无符号的整数(u8,u16,u32,u64)和浮点数(f32,f64)。其他基本类型是bool,string和char。
与可变堆布局类型(认为引用)和数组不同,原始类型在编译时被精确跟踪。输出程序达到3的原因是因为部分求值器会将1和2保留在内存中并将它们加在一起。
在常规语言中,无论是在编译时还是在运行时发生什么,都是模糊不清的,但是Spiral对于此类考虑要谨慎得多。性能是原因之一,但是语言互操作也同样重要。
使用〜可以很容易地指示部分评估器将变量跟踪推向运行时。 inl main()= inl〜x = 1:i64 inl y = 2 x + y 〜称为dyn模式。 在绑定期间,只要变量通过dyn模式传递,它就会被推送到运行时。 在上面的示例中,仅对x进行了染色,因此在编译后的代码中仅生成了let语句。 inl main()= inl〜x = 1:i64 inl〜y = 2 x + y 上述将编译时数据推送到运行时的功能对于函数而言非常重要。 在没有暴露于部分评估的语言(例如F#)中,您可能具有普通的let语句和let内联等效项,仅此而已。 您可以定义一个函数,然后建议编译器对其进行内联,这就是大多数语言中内联的全部内容。 inl加a b = a + binl main()= inl x = 1i32 inl y = 2加x y inl add〜a〜b = a + binl main()= inl x = 1i32 inl y = 2添加x y
inl add〜a〜b:i32 = a + binl main()=添加1 2,添加3 4,添加5 6
设v0:int32 = 1let v1:int32 = 2let v2:int32 = v0 + v1let v3:int32 = 3let v4:int32 = 4let v5:int32 = v3 + v4let v6:int32 = 5let v7:int32 = 6let v8:int32 = v6 + v7struct(v2,v5,v8)
让函数本身成为决定它是否需要运行时变量或编译时间变量的一种方法很方便。
上面的例子很简单,但是对于较大的函数,最好将它们编译为实际方法。完成此操作的方法是将表达式的主体包装在一个连接点中。
inl add〜a〜b:i32 =加入a + binl main()=添加1 2,添加3 4,添加5 6
let rec method0(v0:int32,v1:int32):int32 = v0 + v1 let v0:int32 = 1 let v1:int32 = 2 let v2:int32 = method0(v0,v1)let v3:int32 = 3 let v4: int32 = 4让v5:int32 = method0(v3,v4)让v6:int32 = 5 let v7:int32 = 6让v8:int32 = method0(v6,v7)结构(v2,v5,v8)
连接点将所有传递到其作用域的运行时变量转换为结果编译代码中的方法参数。在将环境作为键的一部分之后进行部分评估时,它会部分评估方法主体(在上述情况下只是a + b)。
为了更好地理解连接点,说明没有参数变色会发生什么会很有帮助。
inl add〜a b:i32 =加入a + binl main()=添加1 2,添加3 4,添加5 6
let rec method0(v0:int32):int32 = v0 + 2和method1(v0:int32):int32 = v0 + 4和method2(v0:int32):int32 = v0 + 6 let v0:int32 = 1 let v1:int32 = method0(v0)let v2:int32 = 3 let v3:int32 = method1(v2)let v4:int32 = 5 let v5:int32 = method2(v4)结构(v1,v3,v5)
inl add〜a b:i32 =加入a + binl main()=添加1 10,添加3 10,添加5 10
let rec method0(v0:int32):int32 = v0 + 10 let v0:int32 = 1 let v1:int32 = method0(v0)let v2:int32 = 3 let v3:int32 = method0(v2)让v4:int32 = 5让v5:int32 = method0(v4)结构(v1,v3,v5)
如果常数b恰好相同,则连接点最终将只需要在编译的代码中使用单个方法。根本就没有消亡,这就是会发生的事情。
inl加a:i32 =加入a + binl main()=加1 2,加3,加5 6
let rec method0():int32 = 3和method1():int32 = 7 and method2():int32 = 11 let v0:int32 = method0()let v1:int32 = method1()let v2:int32 = method2()结构(v0,v1,v2)
这只是为了说明连接点在做什么。在实际的编程实践中,您通常希望内联所有内容或dyn所有参数并将它们包装在连接点中。要执行后者,让我们方便快捷。
让我们添加一个b:i32 = a + binl main()=添加1 2,添加3 4,添加5 6
let rec method0(v0:int32,v1:int32):int32 = v0 + v1 let v0:int32 = 1 let v1:int32 = 2 let v2:int32 = method0(v0,v1)let v3:int32 = 3 let v4: int32 = 4让v5:int32 = method0(v3,v4)让v6:int32 = 5 let v7:int32 = 6让v8:int32 = method0(v6,v7)结构(v2,v5,v8)
inl add〜a〜b:i32 =加入a + b等同于让add a b:i32 = a + b。
这些示例涵盖了连接点的本质。他们所做的只是针对环境表达自己的内容。
尽管它们功能的本质很容易理解,但是这些示例只是在摸索它们的有用性。如果仅仅是表演,它们并不会毫无用处,但是它们不足以激发我将语言作为最基本的功能之一。
遗憾的是,由于Spiral处于alpha阶段,目前我无法演示这种好处,但是值得描述一下它在以前版本中的工作方式。
先前版本的Spiral具有Cuda后端,该后端具有特定于其的连接点变体。
整个工作方式是,编译器将生成一个F#文件,类似于现在的操作,除此之外,还将存在一个C文件,其中包含Cuda连接点专用的方法。该C文件将由Cuda编译器进一步处理为.ptx,这是LLVM使用的高级汇编语言。
Cuda连接点本身不一定会像常规类型那样返回类型,因为F#代码生成器将无法实际运行它,而生成代码来运行它对于它来说太复杂了。取而代之的是,Cuda连接点将返回带有专用方法名称的运行时字符串以及传递到其中的运行时变量数组。
这正是通过Cuda库函数(将其功能公开给其他平台)调用Cuda内核所需的数据。从.NET调用Cuda内核并不像在常规方法中那样简单地将表达式包装在连接点中那样简单,而是有必要将lambda传递给某些运行函数,这几乎很容易。 Cuda连接点是将F#连接到Cuda后端的组成部分。
有了这种机制,就可以轻松制作各种复杂的Cuda内核并从.NET领域进行调用。举例来说,由于Spiral的内联功能,可能具有在GPU上自动区分的地图功能。在开发Spiral之前,我在2016年尝试使用原始F#创建ML库,并完全陷入如何超越Cuda内核成为原始文本字符串的困境。与我之前在Spiral中编写的ML库相比,与F#中的ML库相比,它在质量和可扩展性方面有了重大飞跃,这完全得益于该语言的新颖抽象功能。
在Spiral中,内联是可组合的。不一定是c
......