← 一覧
@lapius/laping-langlaping
GitHubnpm

Laping (.lp)

Lap + Lang = Laping

読んだ順に理解できて、間違いにすぐ気づけることを目指した、C言語製のインタプリタ型プログラミング言語です。 Lexer → Parser → Tree-walking Interpreter という構成で、外部の構文解析ライブラリには依存していません(HTTP通信用に自動更新機能のみ libcurl を使用)。

record Sale(shop, price, qty)
fn Sale.total(s) => s.price * s.qty

sales = [Sale("東京", 120, 30), Sale("大阪", 110, 45), Sale("東京", 400, 8)]

# データは -> で左から右へ流す
tokyo = sales -> filter(fn s => s.shop == "東京") -> map(fn s => s.total()) -> sum()
print("東京の売上: {tokyo:>8} 円")

# 無いかもしれない値は else で既定値に
config = {server: {host: "example.com"}}
port = config.server.port else 8080

# 何をするかを先に、条件は後ろに(後置修飾子)
print("{s.shop}: {s.total()}") for s in sales if s.total() > 3000

# テストは同じ言語で、すぐ横に書ける(laping test で実行)
test "売上を計算できる" {
    expect Sale("東京", 100, 3).total() == 300
}

🤖 AI に Laping のコードを書かせるとき

Laping は新しい言語なので、AI(ChatGPT・Claude・Gemini など)はそのままでは Python や JavaScript の書き方を混ぜてしまいます。AI 向けの仕様書 docs/AI_GUIDE.md を用意しているので、次の指示文を会話の最初に貼り付けてください。

これから Laping(拡張子 .lp)というプログラミング言語でコードを書いてください。
Laping は Python や JavaScript に似ていますが別の言語です。推測で他の言語の書き方を使わず、
次の仕様書に書かれている構文と組み込み関数だけを使ってください。
https://raw.githubusercontent.com/Lapius7/laping-lang/main/docs/AI_GUIDE.md

仕様書を読めない場合は、少なくとも次を守ってください。
- 関数は fn f(a, b) { return a + b } または fn f(a, b) => a + b。無名関数は fn x => x * 2
- ブロック形式の関数は最後の式を自動では返さない。値を返すところには return を書く
- if / for / while の条件に括弧もコロンも付けない: if x > 0 { ... }、for i in 0...n { ... }(... は終端を含まない、.. は含む)
- nil / true / false(None・null・True は無い)。論理演算は and / or / not
- "a" + 1 はエラー。文字列には "{x}" で埋め込む(書式は "{x:.2}"、"{x:>5}")。文字の { は \{ と書く
- 無いかもしれない値は 値 else 既定値(例: m.key else 0)。条件で値を選ぶときは 条件 then a else b
- データは -> でつなぐ: xs -> filter(fn x => x > 0) -> map(fn x => x * 2) -> sum()(x -> f(a) は f(x, a))
- リストへの追加は xs.push(x)、長さは len(xs)。クラスは無いので record Point(x, y) と fn Point.dist(p) => ... を使う
- 関数の中のローカル変数は let x = 1。全部大文字の名前は定数で再代入できない
- 後置修飾子が使える: print(x) if x > 0 / print(x) for x in xs / write("★") repeat 3
- 予約語 repeat is then match などは変数名にしない

AI が Web を読めない場合は、docs/AI_GUIDE.md の中身をそのまま貼り付けても使えます。ルートの llms.txt にも、AI 向けの文書の場所をまとめています。

特徴

設計方針 — なぜこの書き方なのか

Laping の構文は、他の言語の記号をそのまま借りるのではなく、それぞれに「こう書くと楽になる」「こう書くと間違えにくい」という理由があるものにしています。

書き方意味理由
xs -> filter(f) -> sum()左の値を右の関数の第1引数に渡すデータの流れがそのまま矢印の向きになり、書いた順に読める。日本語キーボードで打ちにくい | を使わない
config.port else 8080値が無いときの既定値記号ではなく言葉で書ける。or と違い 0 や "" を置き換えない。途中のキーが無い・nil でもエラーにならない(変数名の打ち間違いはエラーのまま)
x > 0 then "正" else "負"条件で値を選ぶ英文のように読める。?: を覚えなくていい
print(x) for x in xs if x > 0後置の繰り返し既存の後置修飾子(if / unless / while)と同じ「何をするかが先、条件は後」の1ルール
[x * 2 for x in xs if x > 0]内包表記上の後置 for と同じ語順なので、覚えることが増えない
repeat 3 { } / hi() repeat 3回数だけ繰り返す使わないカウンタ変数を書かなくていい
1 < x < 10連鎖比較数学と同じ書き方。各項は1回だけ評価される
record Point(x, y)型の宣言宣言していないフィールドへの代入をエラーにして、打ち間違いにすぐ気づける
fn Point.len(p) / fn string.shout(s)型にメソッドを追加クラスや self なしで、関数と同じ書き方。組み込み型にも追加できる
x is list型の判定type(x) == "lsit" のような打ち間違いを「不明な型名」エラーで防ぐ
MAX_SIZE = 10定数全部大文字の名前は再代入できない。キーワードを増やさずに「変えてはいけない値」を表せる
import "util" as utilモジュール名前の衝突を防ぐ。_ で始まる名前は外から見えない
test "名前" { expect a == b }テスト失敗すると左辺と右辺の値を自動で表示する。通常の実行では飛ばされる
"{点:>5}" / "{率:.1}"書式付きの埋め込み桁揃えや小数の桁数を、文字列の中でそのまま指定できる

インストール

npm(推奨・Linux / macOS / Windows)

npm i -g @lapius/laping-lang

OS と CPU に合ったビルド済みバイナリが入り、laping コマンドが使えるようになります(Node.js 18 以降、C コンパイラ不要)。更新も同じコマンドです(npm 版では laping update は自動更新せず、この更新コマンドを案内します)。

Releaseから取得

Releases から OS に対応するファイルをダウンロードしてください。

Linuxの場合は実行権限を付与してパスの通った場所に置きます。

chmod +x laping-linux-x86_64
mv laping-linux-x86_64 ~/.local/bin/laping

Windowsの場合は laping-windows-x86_64-setup.exe を実行すると、一般的なセットアップウィザード(インストール先選択 → インストール中のプログレスバー → 完了画面)でインストールされます。インストール先は自動でPATHに追加されるため、インストール後はコマンドプロンプトやPowerShellからどのフォルダでも laping コマンドが使えます。スタートメニュー・デスクトップへのショートカット作成、「アプリと機能」からのアンインストールにも対応しています。

laping-windows-x86_64.zip は、インストーラーを使わずにポータブルな形で使いたい場合のみ展開して laping.exe と libcurl-x64.dll を同じフォルダに置いて使います(DLLが無いと起動できません)。

ソースからビルド

libcurl の開発用ヘッダが必要です(自動更新機能のため)。

# Ubuntu/Debian
sudo apt-get install libcurl4-openssl-dev

make
make test      # テストを実行
make install   # ~/.local/bin/laping にインストール

ソースは src/ にあります。

ファイル役割
src/lexer.c字句解析(ソース → トークン列)
src/parser.c構文解析(トークン列 → AST)
src/interp.c評価器(AST を直接たどって実行、スコープ・例外・関数呼び出し)
src/value.c値・文字列・リスト・マップ・GC
src/builtins.c組み込み関数(説明文もここにまとめて書く)
src/ui.c端末表示(色・全角文字の表示幅・エラー表示用のソース保存)
src/main.cコマンドライン・対話モード
src/updater.c自動更新

使い方

laping main.lp              # main.lp を実行
laping main.lp a b c        # 引数付きで実行(スクリプト内では args で受け取れる)
laping                      # 対話モード (REPL) を起動
laping run main.lp          # 実行(laping main.lp と同じ)
laping check *.lp           # 実行せずに構文だけを確認
laping test                 # *_test.lp の test ブロックを実行(フォルダやファイルも指定できる)
laping new myapp            # プロジェクトのひな形(main.lp・lib.lp・main_test.lp)を作る
laping doc                  # 組み込み関数の一覧
laping doc format           # 関数の説明
laping -e 'print(1 + 2)'    # コードを直接実行
laping update               # 最新版を確認し自動更新(npm 版は更新コマンドを表示)
laping version              # バージョン表示
laping help                 # 使い方

端末に出力しているときは、エラーや表が色付きで表示されます。NO_COLOR=1 で色を消し、LAPING_COLOR=always で常に色を付けられます。

対話モード

入力した式の値が色付きで表示されます。{ や ( が閉じていない間や、行末が -> の間は続きの行を入力できます。

laping› xs = [3, 1, 2]
laping› xs -> sort()
=> [1, 2, 3]
laping› fn sq(x) {
   …     return x * x
   … }
laping› sq(12)
=> 144
laping› :time (1..100000) -> sum()
=> 5000050000
  0.004 秒
コマンド説明
:helpコマンド一覧
:vars定義した変数と関数の一覧(型と値)
:doc [関数名]組み込み関数の説明
:load <ファイル>ファイルを読み込んで実行する
:time <コード>実行時間を測る
:reset変数をすべて消す
:clear画面を消す
:q終了(exit() や Ctrl+D でも終了)

テスト

*_test.lp というファイルに test ブロックを書き、laping test で実行します。expect が失敗すると、比較の左辺と右辺の値が表示されます。

# calc_test.lp
fn add(a, b) => a + b

test "足し算ができる" {
    expect add(1, 2) == 3
}
▶ calc_test.lp
  ✓ 足し算ができる
  ✗ わざと失敗
      expect が失敗しました: 左辺は 4、右辺は 5
      場所: calc_test.lp:6

✗ 2 件中 1 件が失敗しました (0.00 秒)

test ブロックは laping test のときだけ実行され、普通に laping calc_test.lp と実行したときは飛ばされます。そのため、関数を定義したファイルに直接テストを書いておくこともできます。

自動更新の仕組み

Release から取得した版で laping update を実行すると、以下の流れで動作します(npm で入れた場合は自動更新せず、npm i -g @lapius/laping-lang を案内します)。

  1. https://api.github.com/repos/Lapius7/laping-lang/releases/latest にアクセスし、最新リリースのタグを取得
  2. 実行中のバージョン(laping --version)と比較
  3. ローカルより新しければ、OSに対応する配布物をダウンロード(Linuxはlaping-linux-x86_64、Windowsはlaping-windows-x86_64.zip)
  4. Windowsの場合は標準搭載のtar.exeでzipを展開し、中のlaping.exeとlibcurl-x64.dllを取り出す(Windows 10 1803以降が必要)
  5. 現在の実行ファイル(とWindowsの場合はDLL)をバックアップしてから新しいものに置き換える

開発者がGitHubに新しいバージョンをタグ付き(vX.Y.Z)でプッシュすると、CI(GitHub Actions)がLinux/Windows向けにビルドしてReleaseへ自動添付します。同じタグから npm 版も自動で公開されます。各ユーザーは laping update(npm 版は npm i -g @lapius/laping-lang)を実行するだけで最新版に追従できます。

構文リファレンス

コメント

# 行コメント
x = 1  # 行末コメントも可

#[
  ブロックコメント
  複数行にわたって書ける
]#

文の区切り

文は改行で区切ります。1行に複数の文を書く場合は ; で区切ります。

a = 1; b = 2; print(a + b)

次の場合は改行しても文が続いているとみなされます。

total = 1 +
    2 +
    3

result = [1, 2, 3, 4]
    .filter(fn(x) => x % 2 == 0)
    .map(fn(x) => x * 10)

値の種類

型 (type())例補足
number10, 3.14, -2, 1_000_000, 0xFF, 0b1010, 1.5e3内部的には double。整数値は整数として表示される
string"hello", 'raw'変更不可。UTF-8
booltrue, false
nilnil「値がない」ことを表す
list[1, "a", [2, 3]]可変長の配列
map{name: "太郎", "age": 20}キーと値の組。挿入順を保持する
range1..5, 0...10数の範囲
functionfn(x) => x * 2, print関数も値として扱える

真偽の判定: false・nil・0・空文字列 ""・空リスト []・空マップ {} は偽、それ以外は真です。

変数と代入

x = 10
name = "Laping"
名前 = "日本語の変数名も使えます"

a, b = 1, 2        # 複数同時に代入
a, b = b, a        # 入れ替え
first, second = ["F", "S"]   # リストを分割して代入

x += 5             # 複合代入: += -= *= /= %=

演算子

優先順位の高い順:

演算子意味
x(...) x[i] x.name呼び出し・添字・フィールド
**べき乗(右結合。-2 ** 2 は -4)
-x !x符号反転・否定
* / // %乗算・除算・切り捨て除算・剰余
+ -加算・減算
.. ...範囲(.. は終端を含む、... は含まない)
== != < > <= >= in not in is is not比較・所属判定・型の判定(1 < x < 10 のように連鎖できる)
not否定
and &&論理積(短絡評価)
or ||論理和(短絡評価)
x -> f(a)パイプライン(f(x, a) と同じ)
条件 then a else b / 値 else 既定値値の選択 / 既定値(条件 ? a : b も使える)

演算子の型の規則(暗黙の型変換はしません):

演算子使える組み合わせ
+数値 + 数値、文字列 + 文字列(連結)、リスト + リスト(連結)
*数値 * 数値、文字列 * 整数(繰り返し)、リスト * 整数(繰り返し)
- / // % **数値のみ。0 での除算・剰余はエラー。% の結果は除数と同じ符号
< > <= >=数値同士、または文字列同士(辞書順)
== !=何でも比較可能。リストとマップは中身で比較。型が違えば常に不一致
in要素 in リスト、キー in マップ、部分文字列 in 文字列、数値 in 範囲

and / or は真偽値ではなく、評価したオペランドの値をそのまま返します。0 や "" も置き換えたくない場合は、次の else を使ってください。

パイプライン ->

x -> f(a, b) は f(x, a, b) と同じ意味です。データを左から右へ流すように書けるので、処理を書いた順に読めます。右側が関数そのもの(x -> sqrt)なら sqrt(x) になります。行頭に -> を書けば、次の行へ続けられます。

[3, 1, 4, 1, 5, 9]
    -> filter(fn x => x > 2)
    -> map(fn x => x * 10)
    -> sort()
    -> print()                 # [30, 40, 50, 90]

既定値 else と、値を選ぶ then / else

値 else 既定値 は、値が nil のときに既定値を使います。さらに左側の a.b.c や a[i] の途中で、nil になった・キーが存在しない・添字が範囲外だった場合も、エラーにせず既定値を使います。変数名の打ち間違い(未定義の変数)は、これまでどおりエラーになります。

config = {server: {port: 8080}}
port = config.server.port else 80           # 8080
host = config.server.host else "localhost"  # キーが無いので "localhost"
first = args[0] else "引数なし"               # 範囲外なので "引数なし"
count = 0 else 10                           # 0(or と違い 0 や "" は置き換えない)
name = user.name else throw "name がありません"  # 無ければエラーにする

条件 then 値1 else 値2 は、条件が真なら値1、偽なら値2 になります。

print(n % 2 == 0 then "偶数" else "奇数")
size = n > 100 then "大" else n > 10 then "中" else "小"

連鎖比較と is

1 < x < 10 は 1 < x and x < 10 と同じ意味です(x は1回だけ評価されます)。is は値の型を調べます。

print(0 <= score <= 100)
print(x is number, xs is list, p is Point, v is not nil)

使える型名は nil bool number string list map function range と、record で宣言した名前です。それ以外の名前はエラーになるので、打ち間違いにすぐ気づけます(x is lsit → 「不明な型名 'lsit'/もしかして 'list' ですか?」)。

文字列

name = "世界"
print("こんにちは、{name}!")          # {式} で埋め込み
print("合計: {1 + 2}、一覧: {[1, 2]}")
print("波かっこそのもの: \{ \}")
print('シングルクオートは {埋め込みしない}')
print("改行\nタブ\t引用符\" 絵文字\u{1F600}")

埋め込みの書式 {式:書式} は [埋め文字][< > ^][幅][.桁数] の形です。全角文字は幅2として揃えます。

名前 = "花子"
点 = 95.5
print("[{名前:<6}] {点:>7.2} 点")   # [花子  ]   95.50 点
print("[{"中央":*^8}]")             # [**中央**]
print("{3.14159:.3}")               # 3.142

同じ書式は format() でも使えます。"..." の中の {} は埋め込みになるため、書式は '...' で書きます: format('{} は {:>5} 点', name, score)

リスト

xs = [3, 1, 4]
print(xs[0], xs[-1], len(xs))
xs[0] = 10
xs.push(5)              # 末尾に追加(push(xs, 5) と同じ)
last = xs.pop()         # 末尾を取り出す
print(xs + [9], [0] * 3)
print(slice(xs, 1, 3))  # 部分リスト

範囲外の添字はエラーになります。要素を増やすときは push / insert を使います。

マップ

person = {name: "太郎", age: 20, "好きな色": "青"}
print(person.name, person["好きな色"])
person.age += 1
person["city"] = "東京"
print(keys(person), "age" in person)
print(get(person, "email", "未登録"))   # キーがなければ既定値
delete(person, "city")
x = 5
point = {x, y: 10}                       # {x: x, y: 10} の省略形

範囲

1..5          # 1, 2, 3, 4, 5(終端を含む)
0...5         # 0, 1, 2, 3, 4(終端を含まない)
range(10)          # 0〜9
range(10, 0, -2)   # 10, 8, 6, 4, 2(増分を指定)
list(1..3)    # [1, 2, 3]

範囲は必要になるまで要素を作らないので、for i in 1..1000000 でも大きなリストは作られません。

出力と入力

print("a", 1, [2])      # 引数をスペース区切りで出力し、改行する
write("改行なし")        # 改行せずに出力
name = input("名前: ")   # 1行読み込む(入力が終わっていれば nil)

条件分岐

if score >= 90 {
    print("A")
} elif score >= 70 {        # else if と書いてもよい
    print("B")
} else {
    print("C")
}

unless logged_in {          # if not と同じ
    print("ログインしてください")
}

繰り返し

while i < 5 { i += 1 }       # 条件が真の間
until i == 0 { i -= 1 }      # 条件が偽の間
loop {                       # 無限ループ(break で抜ける)
    break if done()
}

for x in [10, 20, 30] { print(x) }        # リスト
for i in 1..10 { print(i) }                # 範囲
for ch in "あいう" { print(ch) }           # 文字列(1文字ずつ)
for key in {a: 1, b: 2} { print(key) }     # マップ(キー)
for i, x in ["a", "b"] { print(i, x) }     # 添字と要素
for key, value in {a: 1} { print(key, value) }  # キーと値

break で最も内側のループを抜け、continue で次の繰り返しに進みます。

後置修飾子構文(Laping 独自のスタイル)

単文(代入・式・return・break・continue・throw)の末尾に if / unless / while / until / for / repeat を置くと、条件付きで、または繰り返して実行できます。「何をするか」を先に書き、条件や繰り返しは後ろに書く、という1つのルールです。

print("正の数です") if x > 0
print("0以下です") unless x > 0
return 0 if n < 1
continue unless i % 3 == 0
throw "不正な値" if value < 0
i += 1 while i < 10
i -= 1 until i == 0
print(x) for x in xs                  # 繰り返し
print(x) for x in xs if x > 0         # 条件付きの繰り返し
write("★") repeat 3                   # 回数だけ繰り返し

内包表記

後置の for と同じ語順で、リストやマップを作れます。ループ変数は外に漏れません。

squares = [i * i for i in 1..5]                  # [1, 4, 9, 16, 25]
evens = [x for x in xs if x % 2 == 0]
lengths = {w: len(w) for w in ["apple", "kiwi"]}  # {"apple": 5, "kiwi": 4}
pairs = ["{k}={v}" for k, v in {a: 1, b: 2}]      # ["a=1", "b=2"]

repeat

回数だけ繰り返します。カウンタ変数が要らないときに使います。break / continue も使えます。

repeat 3 {
    print("やあ")
}

match

値によって処理を振り分けます。上から順に調べ、最初に一致した1つだけを実行します。

match value {
    0 => print("ゼロ")
    1, 2, 3 => print("小さい")           # カンマで複数のパターン
    4..9 => print("一桁")                # 範囲に含まれるか
    "hello" => print("あいさつ")
    _ if value < 0 => print("負の数")    # _ は何にでも一致。if で追加条件
    _ => {
        print("その他")                   # ブロックも書ける
    }
}

match は式としても使え、一致した枝の値になります(どれにも一致しなければ nil)。枝は改行かカンマで区切ります。

fn season(m) => match m { 3..5 => "春", 6..8 => "夏", 9..11 => "秋", _ => "冬" }
label = match code {
    200 => "OK"
    404 => "見つかりません"
    _ => "エラー"
}

関数

fn add(a, b) {
    return a + b
}

fn square(x) => x * x               # 式1つだけなら => で書ける

fn greet(name, greeting = "こんにちは") => "{greeting}、{name}さん"   # デフォルト引数
fn total(...nums) => sum(nums)      # 可変長引数(リストで受け取る)

double = fn(x) => x * 2             # 無名関数
triple = fn x => x * 3              # 引数が1つなら括弧を省略できる
[1, 2, 3] -> map(fn x => x + 1)     # 関数を引数に渡す
fn make_counter() {
    let count = 0
    return fn() {
        count += 1
        return count
    }
}
c = make_counter()
c()
print(c())   # => 2

メソッド構文

x.f(a, b) と書くと f(x, a, b) が呼ばれます。組み込み関数にも自分で定義した関数にも使えるため、処理を左から右へつなげて書けます。

fn double(x) => x * 2
print(21.double())                       # double(21)
print("  hi  ".trim().upper())           # upper(trim("  hi  "))
words = "b a c".split().sort().join(",") # "a,b,c"

ただし x がマップで、f という名前のキーに関数が入っている場合はその関数を呼びます。これを使うとオブジェクトのように書けます。

fn Counter() {
    let n = 0
    return {
        inc: fn() { n += 1 },
        value: fn() => n,
    }
}
c = Counter()
c.inc()
print(c.value())   # => 1

record(型の宣言)と型ごとのメソッド

record で、決まったフィールドを持つ型を宣言できます。宣言していないフィールドへの代入はエラーになるので、打ち間違いにすぐ気づけます。

record Point(x, y = 0)             # y は省略すると 0
p = Point(3, 4)
print(p)                           # Point(x: 3, y: 4)
p.x = 10                           # OK
p.z = 1                            # エラー: Point にフィールド 'z' はありません
print(type(p), p is Point)         # Point true

fn 型名.メソッド名(値, ...) で、型にメソッドを追加できます。第1引数がその値です(self は不要)。string list number などの組み込み型にも追加できます。

fn Point.dist(p) => sqrt(p.x ** 2 + p.y ** 2)
print(Point(3, 4).dist())          # 5

fn string.shout(s) => upper(s) + "!"
print("hello".shout())             # HELLO!

x.f(a) は、①型に追加したメソッド ②マップのキー f に入った関数 ③f(x, a) の順に探して呼びます。

定数

全部大文字の名前(2文字以上。MAX_SIZE や API_URL など)は定数になり、2回目の代入はエラーになります。キーワードを増やさずに「変えてはいけない値」を表せます。

MAX_SIZE = 10
MAX_SIZE = 20    # エラー: 定数 MAX_SIZE は変更できません

スコープ

count = 0
fn increment() {
    count += 1        # 外側の count を書き換える
}
fn local_only() {
    let count = 100   # 外側とは別の変数
    return count
}

エラー処理

try {
    risky()
} catch e {             # e にエラーの内容が入る(変数名は省略可)
    print("失敗:", e)
} finally {             # 省略可。成功しても失敗しても必ず実行される
    print("後片付け")
}

throw "値が不正です"             # 任意の値を投げられる
throw {code: 404, message: "Not Found"}

実行時エラー(ゼロ除算・未定義変数など)も catch で捕捉でき、e にはエラーメッセージの文字列が入ります。捕捉されなかったエラーは、行番号と呼び出し履歴を表示して終了コード1で終了します。

Laping: リストの添字 10 が範囲外です(長さ 3) (7行目)
    inner() の呼び出し元: 3行目
    outer() の呼び出し元: 9行目

ファイルの分割(import)

import "lib/util"              # lib/util.lp を読み込んで実行する(.lp は省略可)
import "lib/shapes" as shapes  # モジュールとして読み込む
print(shapes.area(3, 4))

エラー検出

Lapingは曖昧な挙動を避けるため、以下を実行時エラーとして検出します。捕捉されなかったエラーは、場所・該当行・原因の位置(^)・ヒント・呼び出し履歴を表示して、終了コード1で終了します。

エラー: 未定義の変数 'numz'
  --> report.lp:3:28
  |
3 |     return sum(nums) / len(numz)
  |                            ^
  ヒント: もしかして 'nums' ですか?
  呼び出し履歴:
    average() ← 5行目
エラー内容例
未定義の変数を参照print(z)
数値以外の値を数値演算子に渡す"a" - 1
文字列と数値の + 混在"a" + 1
ゼロ除算・ゼロ剰余1 / 0
範囲外の添字・存在しないキー[1, 2][5]、{a: 1}.b
関数でない値の呼び出し・引数の数の間違いnil()、len()
大小比較できない型の比較1 < "a"
止まらない再帰fn f() => f()
record に無いフィールドへの代入Point(1, 2).z = 3
不明な型名x is lsit
定数の書き換えMAX = 1 の後に MAX = 2
文字列リテラルが閉じられていない(構文エラー)print("abc
ブロックの { } が閉じられていない(構文エラー)if x > 0 { print(x)
不正な文字(構文エラー)未対応の記号の使用

構文エラーはプログラムの実行前に検出され、構文エラー: ... と表示されます。laping check を使うと、実行せずに構文だけを確認できます。

組み込み関数

どの関数も f(x, a)・x.f(a)・x -> f(a) のどの形でも呼べます。一覧と説明は laping doc(対話モードでは :doc、プログラム中では help())でも表示できます。

入出力・ファイル

関数説明
print(...)引数をスペース区切りで出力して改行
write(...)改行せずに出力
input(prompt?)1行読み込む。入力が終わっていれば nil
read_file(path)ファイルの中身を文字列で返す
write_file(path, s) / append_file(path, s)ファイルに書き込む / 追記する
file_exists(path)ファイルが存在するか

画面表示

端末に出力しているときだけ色や太字が付き、ファイルやパイプに出すときは文字だけになります。全角文字は幅2として桁を揃えます。

関数説明
table(行のリスト, 見出し?)罫線付きの表を表示する。行はリストかマップ(record)。数値は右寄せ
box(文字列, タイトル?)文字列を枠で囲んで表示する
progress(現在, 全体, 幅?)進捗バーの文字列([██████░░░░] 60%)
pp(値)入れ子のリストやマップを整形して表示する
color(値, 色名)色を付ける(red green yellow blue magenta cyan white gray black、赤 緑 青 なども可)
bold(値) / dim(値) / underline(値)太字 / 薄字 / 下線
confirm(質問)[y/N] で確認し、はいなら true
choose(質問, 選択肢)番号付きの選択肢から選ばせ、選ばれた値を返す
clear_screen()画面を消す
width(値)端末上の表示幅
table([["りんご", 120], ["バナナ", 98]], ["品名", "価格"])
┌────────┬──────┐
│  品名  │ 価格 │
├────────┼──────┤
│ りんご │  120 │
│ バナナ │   98 │
└────────┴──────┘

型・変換

関数説明
type(x)型名("number" "string" "bool" "nil" "list" "map" "range" "function")
str(x)文字列に変換(print と同じ表示)
repr(x)文字列なら引用符付きの表現を返す
num(x)数値に変換("3.5"・"0xFF" も可。変換できなければエラー)
int(x)小数点以下を切り捨てた整数に変換
bool(x)真偽値に変換
is_num(x)数値、または数値に変換できる文字列か
list(x)範囲・文字列・マップ(キー)からリストを作る。リストならコピー
range(end) / range(start, end, step?)end を含まない範囲
len(x)文字列(文字数)・リスト・マップ・範囲の長さ
copy(x)リスト・マップの浅いコピー
to_json(値, インデント?) / from_json(文字列)JSON との変換

リスト

関数説明
push(list, x...)末尾に追加(リスト自身を返す)
pop(list, i?)末尾(または i 番目)を取り出す
shift(list) / unshift(list, x...)先頭を取り出す / 先頭に追加
insert(list, i, x) / remove(list, i)i 番目に挿入 / i 番目を削除して返す
clear(x)リスト・マップを空にする
slice(x, start, end?)部分リスト・部分文字列(負の値は後ろから)
index_of(x, v)見つかった位置。なければ -1
contains(x, v)含まれているか(v in x と同じ)
reverse(x)逆順にした新しいリスト・文字列
sort(x, key_fn?)並べ替えた新しいリスト(安定ソート)。key_fn の値で比較することもできる
map(x, f) / filter(x, f) / each(x, f)各要素に関数を適用。f が2引数なら (要素, 添字) を渡す
reduce(x, f, init?)畳み込み
find(x, f)条件を満たす最初の要素(なければ nil)
any(x, f?) / all(x, f?)いずれか / すべての要素が条件を満たすか
count(x, v_or_f)値に等しい、または条件を満たす要素の数
sum(x) / min(...) / max(...)合計・最小・最大(リスト1つか、複数の引数)
join(x, sep?)要素を文字列にしてつなげる
zip(a, b) / enumerate(x)[[a0, b0], ...] / [[0, x0], ...]
flatten(x) / unique(x)1段平らにする / 重複を除く
first(x) / last(x)最初 / 最後の要素(空なら nil)
take(x, n) / drop(x, n)先頭の n 個 / 先頭の n 個を除いた残り
chunk(x, n)n 個ずつに区切る
group_by(x, f)関数の値ごとにまとめたマップ
partition(x, f)[条件を満たすもの, 満たさないもの]
tally(x)値ごとの出現回数のマップ
min_by(x, f) / max_by(x, f)関数の値が最小 / 最大の要素
is_empty(x)空か(nil も空とみなす)

文字列

関数説明
upper(s) / lower(s)大文字 / 小文字に変換
trim(s)前後の空白(全角スペースを含む)を除く
split(s, sep?)区切り文字で分割。省略すると空白で分割、"" なら1文字ずつ
replace(s, old, new)すべて置換
starts_with(s, p) / ends_with(s, p)前方一致 / 後方一致
chars(s)1文字ずつのリスト
ord(s) / chr(n)文字 ↔ Unicode コードポイント
pad_left(x, width, ch?) / pad_right(...)指定の幅になるまで詰める
fixed(n, digits)小数点以下の桁数を固定した文字列(fixed(3.14159, 2) は "3.14")
format(書式, 値...){} に値を埋め込む。{:>8} で右寄せ、{:.2} で小数2桁
center(x, width, ch?)中央に揃える
capitalize(s) / lines(s)先頭を大文字に / 行ごとに分割

マップ

関数説明
keys(m) / values(m) / items(m)キー / 値 / [キー, 値] のリスト
has(m, k)キーがあるか(k in m と同じ)
get(m, k, default?)値を取得。なければ default(省略時 nil)。リストにも使える
delete(m, k)キーを削除。削除したら true
merge(m1, m2, ...)結合した新しいマップ(後の値が優先)

数学

関数・定数説明
abs floor ceil sqrt exp絶対値・切り捨て・切り上げ・平方根・指数関数
round(x, digits?)四捨五入(桁数指定も可)
pow(x, y)べき乗(x ** y と同じ)
log(x, base?)自然対数(base 指定でその底の対数)
sin cos tan asin acos atan atan2(y, x)三角関数
random()0以上1未満の乱数
rand_int(a, b)a以上b以下の整数の乱数
choice(x) / shuffle(x)ランダムに1つ選ぶ / シャッフルした新しいリスト
seed(n)乱数の種を設定(毎回同じ乱数列にしたいとき)
PI E INF円周率・ネイピア数・無限大

システム

関数・変数説明
argsコマンドライン引数のリスト(laping main.lp a b なら ["a", "b"])
time()現在時刻(UNIX 時間、秒)
date(書式?, 時刻?)日時の文字列(既定は "%Y-%m-%d %H:%M:%S")
env(名前, 既定値?)環境変数を読む
help(関数?)組み込み関数の説明を表示する
clock()プログラム開始からの CPU 時間(秒)
sleep(sec)指定秒数待つ
exit(code?)プログラムを終了する
assert(cond, msg?)条件が偽ならエラーにする

サンプルコード

ファイル内容
examples/tour.lp主な機能をひととおり紹介
examples/todo.lpTODO 管理の CLI アプリ(record・JSON 保存・table・進捗バー)
examples/report.lp売上データの集計レポート(->・group_by・内包表記・書式付き埋め込み)
examples/quicksort.lp再帰とリスト操作によるクイックソート
examples/bank.lpクロージャでオブジェクトを作り、例外で入力を検証する
examples/wordcount.lpマップを使った単語の集計
examples/fizzbuzz.lp / examples/primes.lp / examples/helloworld.lpv1 の基本的なサンプル

文法まとめ(EBNF風)

program     := statement*
statement   := if_stmt | while_stmt | for_stmt | loop_stmt | repeat_stmt | match_stmt
             | try_stmt | fn_decl | record_decl | test_block | import_stmt
             | simple_stmt modifier?                  # 改行 or ";" で終わる
if_stmt     := ("if" | "unless") expr block
               ("elif" expr block | "else" "if" expr block)* ("else" block)?
while_stmt  := ("while" | "until") expr block
for_stmt    := "for" IDENT ("," IDENT)? "in" expr block
loop_stmt   := "loop" block
repeat_stmt := "repeat" expr block
match_stmt  := "match" expr "{" (arm_head (block | simple_stmt))* "}"
arm_head    := pattern ("," pattern)* ("if" expr)? "=>"
pattern     := "_" | expr
try_stmt    := "try" block ("catch" IDENT? block)? ("finally" block)?
fn_decl     := "fn" (IDENT ".")? IDENT fn_rest            # fn Type.method(...)
record_decl := "record" IDENT "(" params ")"
test_block  := "test" STRING block
import_stmt := "import" STRING ("as" IDENT)?
block       := "{" statement* "}"

simple_stmt := "let" IDENT ("," IDENT)* ("=" expr ("," expr)*)?
             | "return" expr? | "break" | "continue" | "throw" expr | "expect" expr
             | target ("," target)* "=" expr ("," expr)*
             | target ("+=" | "-=" | "*=" | "/=" | "%=") expr
             | expr
target      := IDENT | postfix "[" expr "]" | postfix "." IDENT
modifier    := ("if" | "unless" | "while" | "until") expr   # simple_stmt と同じ行のみ有効
             | "for" IDENT ("," IDENT)? "in" expr ("if" expr)?
             | "repeat" expr

expr        := pipe ("then" expr "else" expr | "else" expr | "?" expr ":" expr)?
pipe        := or_expr ("->" or_expr)*                   # x -> f(a) は f(x, a)
or_expr     := and_expr (("or" | "||") and_expr)*
and_expr    := not_expr (("and" | "&&") not_expr)*
not_expr    := "not" not_expr | comparison
comparison  := range ((cmp_op range)+ | ("in" | "not" "in") range | "is" "not"? TYPE)*
cmp_op      := "==" | "!=" | "<" | ">" | "<=" | ">="     # 連鎖できる: 1 < x < 10
range       := additive ((".." | "...") additive)?
additive    := term (("+" | "-") term)*
term        := unary (("*" | "/" | "//" | "%") unary)*
unary       := ("-" | "+" | "!") unary | power
power       := postfix ("**" unary)?
postfix     := primary ("(" args ")" | "[" expr "]" | "." IDENT ("(" args ")")?)*
primary     := NUMBER | STRING | "true" | "false" | "nil" | IDENT | "(" expr ")"
             | "[" (expr ("," expr)* ","? | expr "for" for_clause)? "]"
             | "{" (entry ("," entry)* ","? | expr ":" expr "for" for_clause)? "}"
             | "fn" IDENT? fn_rest | "match" expr "{" (arm_head expr)* "}"
             | "throw" expr
for_clause  := IDENT ("," IDENT)? "in" expr ("if" expr)?
entry       := IDENT ":" expr | IDENT | expr ":" expr
fn_rest     := "(" params? ")" ("=>" expr | block) | IDENT "=>" expr
params      := param ("," param)*
param       := IDENT ("=" expr)? | "..." IDENT

予約語: if elif else unless while until for in loop repeat break continue return fn let true false nil and or not is then match try catch finally throw import

文の先頭の record test expect と、import の後の as は、その位置でだけ特別な意味を持ちます(変数名としても使えます)。

v2.0 からの変更点(v2.1)

v2.0 のプログラムはほぼそのまま動きますが、次の点が変わっています。

v1 からの変更点(v2.0)

v1 のプログラム(examples/fizzbuzz.lp など)はそのまま動きますが、次の点が変わっています。

開発者向け

テスト

make test

tests/*.lp を実行し、出力を同名の .expected ファイルと比較します(*_test.lp は laping test で実行します。終了コードは既定で 0、名前.exit があればその値を期待します)。GC の不具合を探すときは、すべての文の実行前に GC を走らせる LAPING_GC_STRESS=1 を付けて実行してください。

LAPING_GC_STRESS=1 tests/run_tests.sh ./laping

AI 向け仕様書の更新

組み込み関数を追加・変更したら、docs/AI_GUIDE.md の一覧を src/builtins.c の関数表から作り直してください。CI でも一覧が最新かを確認しています。

python3 tools/update_ai_guide.py

新バージョンのリリース方法

git tag v2.1.0
git push origin v2.1.0

タグをプッシュすると GitHub Actions が起動し、テストを実行したうえで Linux/Windows向けバイナリをビルドして自動的にReleaseへ添付します。各ユーザーは laping update を実行するだけで新しいバイナリに更新されます。タグのバージョンは src/laping.h の LAPING_VERSION と合わせてください。

ライセンス

(未設定)