Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

lys

Package Overview
Dependencies
Maintainers
2
Versions
39
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

lys

  • 0.1.13
  • latest
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
3
increased by200%
Maintainers
2
Weekly downloads
 
Created
Source







Lys, a language that compiles to WebAssembly.

Read more about it in this blog post.

Where to start?

  • To learn what can be used so far: browse the standard library
  • To learn how real code looks like: browse the execution tests
  • To learn how high level constructs get compiled: browse the sugar syntax tests
  • To start developing it locally, I do make watch and then I run the tests in other terminal with make snapshot
  • To see an example project: browse the keccak repo

Getting started

For the time being I'll use npm to distribute the language.

  1. npm i -g lys

  2. Create a folder and a file main.lys

    import support::env
    
    #[export]
    fun test(): void = {
      support::test::START("This is a test suite")
    
      printf("Hello %X", 0xDEADBEEF)
      support::test::mustEqual(3 as u8, 3 as u16, "assertion name")
    
      support::test::END()
    }
    
  3. Run lys main.lys --test --wast. It will create main.wasm main.wast and will run the exported function named test.

How does it look?

Structs & Implementing operators

struct Vector3(x: f32, y: f32, z: f32)

impl Vector3 {
  fun -(lhs: Vector3, rhs: Vector3): Vector3 =
    Vector3(
      lhs.x - rhs.x,
      lhs.y - rhs.y,
      lhs.z - rhs.z
    )

  #[getter]
  fun length(this: Vector3): f32 =
    f32.sqrt(
      this.x * this.x +
      this.y * this.y +
      this.z * this.z
    )
}

fun distance(from: Vector3, to: Vector3): f32 = {
  (from - to).length
}

Pattern matching

// this snippet is an actual unit test

import support::test

enum Color {
  Red
  Green
  Blue
  Custom(r: i32, g: i32, b: i32)
}

fun isRed(color: Color): boolean = {
  match color {
    case is Red -> true
    case is Custom(r, g, b) -> r == 255 && g == 0 && b == 0
    else -> false
  }
}

#[export]
fun main(): void = {
  mustEqual(isRed(Red), true, "isRed(Red)")
  mustEqual(isRed(Green), false, "isRed(Green)")
  mustEqual(isRed(Blue), false, "isRed(Blue)")

  mustEqual(isRed(Custom(255,0,0)), true, "isRed(Custom(255,0,0))")
  mustEqual(isRed(Custom(0,1,3)), false, "isRed(Custom(0,1,3))")
  mustEqual(isRed(Custom(255,1,3)), false, "isRed(Custom(255,1,3))")
}

Algebraic data types

// this snippet is an actual unit test

enum Tree {
  Node(value: i32, left: Tree, right: Tree)
  Empty
}

fun sum(arg: Tree): i32 = {
  match arg {
    case is Empty -> 0
    case is Node(value, left, right) -> value + sum(left) + sum(right)
  }
}

#[export]
fun main(): void = {
  val tree = Node(42, Node(3, Empty, Empty), Empty)

  support::test::mustEqual(sum(tree), 45, "sum(tree) returns 45")
}

Types and overloads are created in the language itself

The compiler only knows how to emit functions and how to link function names. I did that so I had fewer things hardcoded into the compiler and allows me to write the language in the language.

To do that, I had to add either a %wasm { ... } code block, and a %stack { ... } type.

  • %wasm { ... }: can only be used as a function body, not as an expression. It is literally the code that will be emited to WAST. The parameter names remain the same (prefixed with $ as WAST indicates). Other symbols can be resolved with fully::qualified::names.

  • %stack { wasm="i32", size=4 }: it is a type literal, it indicates how much memory should be allocated in structs (size) and what type to use in locals and function parameters (wasm, it needs a better name).

/** We first define the type `int` */
type int = %stack { wasm="i32", size=4 }

/** Implement some operators for the type `int` */
impl int {
  fun +(lhs: int, rhs: int): int = %wasm {
    (i32.add (local.get $lhs) (local.get $rhs))
  }
  fun -(lhs: int, rhs: int): int = %wasm {
    (i32.sub (local.get $lhs) (local.get $rhs))
  }
  fun >(lhs: int, rhs: int): boolean = %wasm {
    (i32.gt_s (local.get $lhs) (local.get $rhs))
  }
}

fun fibo(n: int, x1: int, x2: int): int = {
  if (n > 0) {
    fibo(n - 1, x2, x1 + x2)
  } else {
    x1
  }
}

#[export "fibonacci"] // "fibonacci" is the name of the exported function
fun fib(n: int): int = fibo(n, 0, 1)

Some sugar

Enum types

enum Tree {
  Node(value: i32, left: Tree, right: Tree)
  Empty
}

Is the sugar syntax for

type Tree = Node | Empty

struct Node(value: i32, left: Tree, right: Tree)
struct Empty()

impl Tree {
  fun is(lhs: Tree): boolean = lhs is Node || lhs is Empty
  // ...
}

impl Node {
  fun as(lhs: Node): Tree = %wasm { (local.get $lhs) }

  // ... many methods were removed for clarity ..
}

impl Empty {
  fun as(lhs: Node): Tree = %wasm { (local.get $lhs) }
  // ...
}

is and as operators are just functions

impl u8 {
  /**
   * Given an expression with the shape:
   *
   *   something as Type
   *   ^^^^^^^^^    ^^^^
   *        $lhs    $rhs
   *
   * A function with the signature:
   *     fun as($lhs: LHSType): $rhs = ???
   *
   * Will be searched in the impl of LHSType
   *
   */


  fun as(lhs: u8): f32 = %wasm { (f32.convert_i32_u (local.get $lhs)) }
}

fun byteAsFloat(value: u8): f32 = value as f32
struct CustomColor(rgb: i32)

type Red = void
impl Red {
  fun is(lhs: CustomColor): boolean = match lhs {
    case is Custom(rgb) -> (rgb & 0xFF0000) == 0xFF0000
    else -> false
  }
}

var x = CustomColor(0xFF0000) is Red

// this may not be a good thing, but you get the idea

There are no dragons behind the structs

The struct keyword is only a high level construct that creats a type and base implementation of something that behaves like a data type, normally in the heap.

struct Node(value: i32, left: Tree, right: Tree)

Is the sugar syntax for

// We need to keep the name and order of the fields for deconstructors

type Node = %struct { value, left, right }

impl Node {
  fun as(lhs: Node): Tree = %wasm {
    (local.get $lhs)
  }

  #[explicit]
  fun as(lhs: Node): ref = %wasm {
    (local.get $lhs)
  }

  // the discriminant is the type number assigned by the compiler
  #[inline]
  private fun Node$discriminant(): u64 = {
    val discriminant: u32 = Node.^discriminant
    discriminant as u64 << 32
  }

  // this is the function that gets called when Node is used as a function call
  fun apply(value: i32, left: Tree, right: Tree): Node = {
    // a pointer is allocated. Then using the function `fromPointer` it is converted
    // to a valid Node reference
    var $ref = fromPointer(system::core::memory::calloc(1 as u32, Node.^allocationSize))
    property$0($ref, value)
    property$1($ref, left)
    property$2($ref, right)
    $ref
  }

  // this function converts a raw address into a valid Node type
  private fun fromPointer(ptr: u32): Node = %wasm {
    (i64.or (call Node$discriminant) (i64.extend_i32_u (local.get $ptr)))
  }

  fun ==(a: Node, b: Node): boolean = %wasm {
    (i64.eq (local.get $a) (local.get $b))
  }

  fun !=(a: Node, b: Node): boolean = %wasm {
    (i64.ne (local.get $a) (local.get $b))
  }

  #[getter]
  fun value(self: Node): i32 = property$0(self)
  #[setter]
  fun value(self: Node, value: i32): void = property$0(self, value)

  #[inline]
  private fun property$0(self: Node): i32 = i32.load(self, Node.^property$0_offset)
  #[inline]
  private fun property$0(self: Node, value: i32): void = i32.store(self, value, Node.^property$0_offset)

  #[getter]
  fun left(self: Node): Tree = property$1(self)
  #[setter]
  fun left(self: Node, value: Tree): void = property$1(self, value)

  #[inline]
  private fun property$1(self: Node): Tree = Tree.load(self, Node.^property$1_offset)
  #[inline]
  private fun property$1(self: Node, value: Tree): void = Tree.store(self, value, Node.^property$1_offset)

  #[getter]
  fun right(self: Node): Tree = property$2(self)
  #[setter]
  fun right(self: Node, value: Tree): void = property$2(self, value)

  #[inline]
  private fun property$2(self: Node): Tree = Tree.load(self, Node.^property$2_offset)
  #[inline]
  private fun property$2(self: Node, value: Tree): void = Tree.store(self, value, Node.^property$2_offset)

  fun is(a: (Node | ref)): boolean = %wasm {
    (i64.eq (i64.and (i64.const 0xffffffff00000000) (local.get $a)) (call Node$discriminant))
  }

  fun store(lhs: ref, rhs: Node, offset: u32): void = %wasm {
    (i64.store (i32.add (local.get $offset) (call addressFromRef (local.get $lhs))) (local.get $rhs))
  }

  fun load(lhs: ref, offset: u32): Node = %wasm {
    (i64.load (i32.add (local.get $offset) (call addressFromRef (local.get $lhs))))
  }
}

Build Status Build Status

FAQs

Package last updated on 24 Dec 2022

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc