项目作者: Laythe-lang

项目描述 :
A dynamics typed language originally based on the crafting interpreters series
高级语言: Rust
项目地址: git://github.com/Laythe-lang/Laythe.git
创建时间: 2019-10-22T12:41:58Z
项目社区:https://github.com/Laythe-lang/Laythe

开源协议:MIT License

下载


Laythe

A gradual typed, object oriented, fiber based concurrent scripting language. Laythe’s goal is to
seemlessly transistion from a single file untyped script to fully typed and fast production project.
This is planned to be achieved by including a Typescript like gradual compiler that can eventually
be used to JIT faster code.

The language was originally based on the 2nd book of Crafting Interpreters. See git tag v0.1.0 for a fully compliant lox implementations.

Getting Started

Laythe is built in rust and as such uses the typical set of cargo commands for building, testing, running and benchmarks. If you don’t have cargo on your system it us recommended you us rustup to get setup.

Build debug

  1. cargo build

Build Release

  1. cargo build --release

Run Test Suite

  1. cargo test

Run Benchmark Suite

  1. cargo bench

Run Repl

  1. cargo run [--release]

Run a File

  1. cargo run [--release] [filepath]

Language Overview

As stated at the top Laythe adjectives are

  • Gradually Typed
  • Object Oriented
  • Fiber (Coroutine) based concurrency

Basic Types

Today laythe supports a small number of basic types.

  1. // ---- boolean ----
  2. true;
  3. false;
  4. // ---- nil ----
  5. nil;
  6. // ---- numbers ----
  7. // single number type IEEE 64 bit float point
  8. // this may change in the future but for now al
  9. 10.5;
  10. 3;
  11. // ---- strings ----
  12. "this is a string";
  13. // with interpolation
  14. "this is a string with interpolation ${10}";

Collection

Laythe support two collection types lists and maps

  1. // ---- lists ----
  2. // list can be homogeneous
  3. let l1 = [1, 2, 3];
  4. print(l1[2]);
  5. laythe:> 3
  6. print(l1[-1]);
  7. laythe:> 3
  8. // or heterogeous
  9. let l2 = ['foo', nil, []];
  10. print(l2[0]);
  11. laythe:> 'foo'
  12. // ---- maps ----
  13. // maps which can use any value as key and value
  14. let list = [];
  15. let m = {
  16. 1: 2,
  17. nil: 'some string',
  18. 'some string': false,
  19. list: nil,
  20. };
  21. print(m[list]);
  22. laythe:> nil

Function

Functions in Laythe are first class values and support closure capture.

  1. // ---- function statement ----
  2. fn doubler(x) {
  3. x * 2
  4. }
  5. // ---- function expressions / lambdas ----
  6. // with expression body
  7. |x| x * 2;
  8. // with block body
  9. |x| { x * 2 };
  10. // ---- recursion ----
  11. fn fib(n) {
  12. if n < 2 { return n; }
  13. fib(n - 2) + fib(n - 1)
  14. }
  15. // ---- closure capture ----
  16. let name = "John";
  17. fn greeter() {
  18. print("Hello ${name}");
  19. }
  20. // ---- first class values ----
  21. fn halver(x) { x / 2 }
  22. fn invoker(fn, val) { fn(val) }
  23. print(invoker(halfer, 10));
  24. laythe:> 5

Classes

  1. // ---- classes ----
  2. // class with no explicit super class no initializer
  3. class A {}
  4. // create an instance by call the class
  5. let a = A();
  6. class B {
  7. // constructor
  8. init(x) {
  9. self.x = x
  10. self.z = 'some string';
  11. }
  12. // instance method
  13. bar() {
  14. self.x
  15. }
  16. // static method
  17. static baz() {
  18. print("hello from B")
  19. }
  20. }
  21. // calling the class class init if present
  22. let b = B('cat');
  23. // call an instance method
  24. print(b.bar());
  25. laythe:> 'cat'
  26. // call a class method
  27. B.baz();
  28. laythe:> "hello from B"
  29. // access a property
  30. print(b.x);
  31. laythe:> "cat"
  32. // subclass B as C
  33. class C : B {
  34. init(x, y) {
  35. super.init(x)
  36. self.y = y;
  37. }
  38. bar() {
  39. // call super method bar
  40. super.bar() + 3
  41. }
  42. foo() {
  43. 'y is ${self.y}'
  44. }
  45. }
  46. let c = C(10, nil);
  47. print(c.bar());
  48. laythe:> 13
  49. print(c.foo());
  50. laythe:> 'y is nil';

Control Flow

Laythe has the faily standard set of control flow you’d find in an scripting language

  1. // ---- conditional flow ----
  2. if 10 > 3 {
  3. // predicate met branch
  4. }
  5. if 10 == nil {
  6. // predicate met branch
  7. } else {
  8. // alternative branch optional
  9. }
  10. // condtional expressions (ternary)
  11. let y = 10 > 3
  12. ? 'cat'
  13. : 'dog';
  14. // ---- while loop ----
  15. let x = 0;
  16. while x < 10 {
  17. x += 1;
  18. }
  19. print(x)
  20. laythe:> 10
  21. // ---- for loop ----
  22. let sum = 0;
  23. // anything the implmenents the iterator interface
  24. for y in [1, 2, 3] {
  25. sum += y;
  26. }
  27. print(sum);
  28. laythe:> 6

Type Annotations

Laythe now supports a basic set of type annotations. Long term this will eventually turn into gradual typing,
but the parser will now ingest some Typescript like annotations.

  1. // let type def
  2. let b: string = "example"
  3. // essentially equivalent
  4. let f: (a: string) -> number = |a| Number.parse(a);
  5. let f = |a: string| -> number Number.parse(a);
  6. // function signature
  7. fn adder(x: number, y: number) -> number {
  8. return x + y
  9. }
  10. // class type param
  11. class Foo<T> {
  12. // field declaration
  13. bar: T;
  14. init(bar: T) {
  15. self.bar = bar;
  16. }
  17. }
  18. // type def
  19. type Holder<T> = Foo<T>[];
  20. // also called interface in TS
  21. trait NumHolder {
  22. holder: Holder<number>;
  23. }

Fibers and Channels

Laythe concurrent model builds around fibers and channels. Fibers are known by a number of terms such as green threads, and stackfull coroutines. These are separate and lightweight units of execution that in many ways act like an os thread. Laythe currently only runs a single fiber at a time switch between fibers on channel send’s and receives.

  1. fn printer(ch) {
  2. print(<- ch)
  3. }
  4. fn writer(ch) {
  5. let strings = ["foo", "bar", "baz"];
  6. for i in 100.times() {
  7. for string in strings {
  8. ch <- string;
  9. }
  10. }
  11. }
  12. // create a buffered channel that can hold 10 elements
  13. let ch = chan(10);
  14. // "launch" the printer function as a fiber passing in the channel
  15. launch printer(ch)
  16. // send 3 strings to the channel to eventually be written
  17. ch <- "foo";
  18. ch <- "bar";
  19. ch <- "baz";
  20. // send many string to the channel to eventually be written
  21. writer(ch);

Performance

Here I ran the essentially the simple benchmark suite from crafting interpretors on Laythe,
Ruby and Python. Later I may implement some more standard benchmarks to get a more
holistic view.

These benchmarks where run on a 2019 MBP

Timings

benchmark ruby 3.0.2 python 3.9.12 laythe
binary_trees 1.50 44.07 1.87
equality 8.12 3.84 1.54
fib 0.78 2.73 1.31
instantiation 1.52 1.565628052 1.65
invocation 0.25 0.98 0.52
list 1.47 3.30 2.87
method_call 0.12 0.66 0.22
properties 0.28 1.52 0.62
trees 1.41 7.45 2.21
zoo 0.23 1.18 0.50

Percent Speed

Here I show how fast or slow laythe is relative to python and ruby. Here I simply
take the ratio of other_lang/laythe * 100. laythe pretty much sits right between
ruby and python in these benchmarks, easily beating python and being easily beaten
by ruby.

benchmark ruby 3.0.2 python 3.9.12 laythe
binary_trees 80.2% 2355.1% 100%
equality 524.3% 248% 100%
fib 59.9% 207.8% 100%
instantiation 92.2% 94.5% 100%
invocation 48.5% 186.5% 100%
list 51.4% 115.2% 100%
method_call 57.8% 297.9% 100%
properties 44.9% 244.1% 100%
trees 63.9% 336.1% 100%
zoo 46.1% 235.6% 100%

Future Ideas

These are some features / changes I’m considering adding to the language.

Gradual typing

I’m a huge fan of Typescript and believe it’s gradual typing approach is quite fantastic. I’d like to incorporate a similar system here. Beyond just the normal type errors I’d like to have more of a runtype check at typed and untyped boundaries, at least enable that with a flag.

  1. // Possible type syntax
  2. fn unTyped(x) {
  3. return typed(x)
  4. }
  5. fn typed(x: number) -> string {
  6. return typedInner(x);
  7. }
  8. fn typedInner(x: number) -> string {
  9. return x.str();
  10. }
  11. > unTyped(10);
  12. '10'
  13. > unTyped(20);
  14. '20'
  15. unTyped('15')
  16. typeError 'typed' expected number received string'

Here I would think it could be useful if laythe automatically injected some runtime type checking before it’s called in unTyped. The runtime check could then be emitted inside of typed as we know the value comes from a trust checked typedInner. I think this approach would really help extend the usefulness of the typescript model. I know outside of the strict flags in ts that you know the wrong type might still slip through. Here I think we can inject runtime checks to assert call boundaries are truly what the claim to be.

Placeholder partial application

I think placeholder partial application could really help prompt more first class function patterns. I think this is a small piece of syntactic sugar that could really cut down on a lot of boiler plate

  1. fn sum(a, b) { a + b }
  2. fn eq(a, b) { a == b }
  3. // possible today
  4. let increment1 = |x| sum(1, x);
  5. // idea
  6. let increment2 = sum(1, _);
  7. print(increment1(10));
  8. // 11
  9. print(increment2(10));
  10. // 12
  11. // I think the nicer usecase is inline
  12. // take a list of number increment by 2 and collect into
  13. // a new list
  14. [1, 2, 3].iter().map(sum(_, 2)).into(List.collect);

JIT

Eventually I’d like to use the available type information to eventually allow the runtime
to be JIT’d. This is a very far off goal but that would be the ideal endstate.