Skip to content
todesking edited this page Oct 18, 2024 · 30 revisions

プログラミング言語Borlang

⚠️ このドキュメントは想像に基づいて書かれているため、実際の実装とはあまり一致していません。

Borlangとは何か

Borlangは新しい型システムの実験場として開発されたインタプリタ言語です。

複雑な型システムを作りたい場合、相応に複雑な言語仕様が要求されます。 Borlangはその要求に応えるため、そのへんの言語にありがちな機能を全部搭載することを目標としています。すなわち、

  • オブジェクト指向
  • 高階関数
  • エフェクト
  • ジェネレータ
  • 例外
  • Enum
  • レコード
  • 型クラス
  • モジュール
  • パターンマッチ
  • オーバロード

などなど。

こいつにTypeScriptを複雑にしてSMTソルバー乗せたような型システムを乗せるんじゃ……。

方針

  • ランタイムは正しく動けば十分なのでナイーブな実装でよい。
  • 文法はなるべく工夫せずにありがちなものを採用する。
  • 機能がたくさんあると偉い。

コマンド

borlang {path_to_module_root} [module.][target] で指定した関数を実行します。 module省略時にはmainモジュール、target省略時にはmain関数が使用されます。

// src/command/main.borlang

pub fn main() {
  println("Hello");
}
$ borlang src command/main.main
Hello

borlang cliでREPLが実行されます。--path=path1,..オプションでモジュールのルートパスを追加できます。

$ borlang cli
> 1 + 1
=> 2

機能

コメント

// comment 形式のみ対応。

モジュール

1ファイルが1モジュールとして扱われます。

// m1/foo.borlang
// module name: "m1/foo"

pub let value = 1;

import式で他のモジュールを参照できます。(モジュール名として指定できるのは文字列リテラルのみ)

// m2/bar.borlang

let {value} = import "m1/foo";

リテラル

  • Int: 1
  • Double: 1.0
  • String: "foo"
  • Template: `foo ${x}`
  • Tagged template: sql`select * from users where id = ${id}`
  • Record: {a: 1, [key]: value, key_and_value, ..other_record}
  • Array: [1, 2, ...another_array]

Record, Arrayについてはmutabilityの指定できてほしい気がする。

変数束縛

let x = 1;
let mut y = [1, 2, 3];

同じ名前を定義するとシャドウイングします。

C++でいうところのcont && constの区別をつけたい気がする。

制御構造

if

if(expr_cond) expr_th else expr_el;

ループ

for(let i = 0; i < N; i++) {
}

for(let x in EXPR) {
}

while(EXPR) {
}

breakcontineが使えます。

パターンマッチ

x match {
  PATTERN => EXPR,
}

continue EXPR できる。

パターン:

  • 各種リテラル
  • 識別子: その名前でキャプチャされる
  • _: dont care
  • レコード
    • {key: PATTERN}: Exactな形状でマッチ
    • {key: PATTERN, ..}: 他のメンバを許す
    • {key: Some(_)@k, ..@rest}: キャプチャ
  • Some(n) None Foo{x: 1} : Enum
  • [1, 2, ..]: Array
x match PATTERN; // booleanが得られる

if(let PATTERN = EXPR) EXPR else EXPR;

while(let PATTERN = EXPR) EXPR;

for(let PATTERN in EXPR) EXPR;

let PATTERN = EXPR; // 必ず成功する場合のみ
let PATTERN = EXPR else EXPR;

ブロック

{
  EXPR;
  EXPR;
  EXPR // セミコロンをつけないEXPRが末尾だと、それがブロックの値となる
}

関数

fn f<T: TC>(arg: T, ..args: T[]): Int throw IOError perform IOEffect {
  // ...
}

アノテーション

ジェネレータ

fn *range(n: Int): yield Int {
  for(var i = 0; i < n; i++) {
    yield i;
  }
}

for(val i in range(10)) {
 println(i);
}

ほとんどエフェクトで模倣できるんだけど、range(100)の結果はGenerator<Int>型になって任意のタイミングで再開できる必要があるため専用構文はやはり必要か。

クラス

class A in B & C {
  // primary constructor
  this(val a: Int) {
  }
  this() { this(0) }
  fn get_a(): Int = this.a;
}

val a = new A();

インタフェース

interface Foo {
  fn foo(): Int;
}

class FooImpl in Object & Foo {
  impl fn foo(): Int = 42;
}

型クラス

インタフェースと相互運用可能な仕組みを入れたい。

type class ToString {
  fn to_string(this): String;
}

enum Foo();
impl ToString for Foo {
  fn to_string(this): String { "Foo" }
}

type class Functor for M<_> {
  fn pure<T>(v: T): M<T>;
  fn fmap<T, U>(this: M<T>, f: T => U): M<U>;
}

impl <T> Functor for T[] {
  fn pure(): Self = [];
  fn map<U>(this, f: T => U): U[]  = ...;
}

val a = <Functor for Array>.pure<Int>();
val foo = [1, 2, 3]~map(_ + 1);

fn zerofill<F: Functor, X>(f: F<X>): F<Int> {
  f.map(x => 0)
}

例外

Exceptionクラスを継承したものをthrowできる。

enum ParseError in Exception {
  // ...
}
fn parse(s: String): Expr throw ParseError {
  if(s[0] != '!') throw ParseError.Foo();
}

fn main(): throw Exception {
  val expr = parse(read_line()) catch {
    case ParseError.Foo() => // ...
  }
}

レコード

レコードリテラルで作れる値。Recordのサブクラスになる。

val r = {a: 1};

r.a // 1

val r = {..r, b: 2}; // r = {a: 1, b: 2}

Newtype

type new Foo(String) {
  // Foo(s)を呼べるのはメンバのみ
  fn parse(s: String): Foo = Foo(s);
}

Enum

// 1要素のenum
enum Email(String);

enum Option<T> {
  case Some(T);
  case None;
}

// これらの型がどうなるかはこれから考える
val Some = Option.Some;
val None = Option.None;

// GADTs
enum Expr<T> {
  // 「なるべく既存の文法を使う」と言いつつ、extendsと書きたくないのでinにしてしまいました。
  case Plus(Expr<Int>, Expr<Int>) in Self<Int>;
  case Str(T) in Self<String>;
  case Num(Int) in Self<Int>;
}

エフェクト

Effect<T>クラスを継承したものをperformできる。

関数はperformから最大1回復帰する。継続について考えたくないため。

ハンドラ内でのエフェクトや例外は外側に飛ぶ。

class Effect {
  type T;
}
enum Console in Effect {
  case ReadLine() { impl type T = String; }
  case Write(String) { impl type T = Unit; }
}

// path dependent typeが出てきてしまった……
fn perform<E in Effect>(e: E): E.T;

// {x in T | x not in U} を表す型演算が出てきてしまった……
fn handle<E in Effect, HE in E, T>(h: HE => E.T, f: () => T perform E): T perform E ! HE;

fn read_line(): String perform Console {
  perform(ReadLine())
}

// whereが出てきてしまった……
fn use_console<T, E>(f: () => T perform E): T perform E ! Console
where E in Effect, Console in E
{
  handle(_ match {
    Console.ReadLine() => internal.read_line(),
    Console.Write(s) => internal.write(s),
  }, () => { read_line() })
}

Async/Await

これもジェネレータと同じく、関数を呼んだときに処理中であることを表現する値が返ってくるべきなので専用構文がほしい。 ジェネレータとは制御の経路が違うためジェネレータとも統一できない。どうにかならんか?

マクロ

多段階計算に手を出さないことを当面の目標としたい(全然知らないので)

  • Literal types for Int/String
  • Union(|), Intersection(&)
  • Exclude(T ! U) ←マジで
  • Record
    • {key1: T1, key2: T2, ..T3, ..}
    • {a: 1}(a: 1のみ持つrecord)と {a: 1, ..}(少なくともa: 1を持つrecord)は区別される。
    • Subtype関係は {a: 1, b: 1} <: {a: 1, ..} <: {..} <: {}, ({} | {a: Int}) <: {a?: Int} といった具合。
      • 健全性のため、{} <: {a?: Int}だが、{..} !<: {a?: Int}になる。
  • Top(Object), Bottom(Never)
  • Fail<Payload>: 評価中に出現するとコンパイルエラー。
  • Type fn
    • type fn ArgumentOf<X in AnyFunction> = X match { case (..args): _ => args}