This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

mlang / README.md
6.3 kB

mlang#

A small functional language prototype written in Lean 4.

Current scope:

  • integer, decimal, and boolean literals
  • variables
  • let
  • if
  • typed lambdas
  • function application
  • decimal arithmetic with implicit Int promotion
  • strings
  • native parsed data values
  • lexical closures
  • hand-written lexer and parser
  • static type checker
  • implicit effect tracking
  • builtin file IO
  • builtin Nickel evaluation

The interpreter lives in Mlang/Interpreter.lean. The parser lives in Mlang/Parser.lean.

Run#

With devenv installed:

devenv shell
build
run

You can also run commands directly without entering the shell:

devenv test
devenv shell -- build
devenv shell -- run

The executable supports a REPL and file execution:

devenv shell -- lake exe mlang
devenv shell -- lake exe mlang --eval "1 + 2"
devenv shell -- lake exe mlang examples/samples.mlg

Current syntax:

expr ::= let name = expr in expr
       | if expr then expr else expr
       | try expr with name => expr
       | pmap name in expr => expr
       | -> expr ident*
       | fun (name : type) => expr
       | expr = expr
       | expr + expr
       | expr - expr
       | expr * expr
       | expr expr
       | [expr, ...]
       | { key = expr, ... }
       | null
       | (expr)
       | true | false | 123 | 1.23 | "text" | name

program ::= expr (';' expr)*

type ::= Int | Decimal | LazyReal | Bool | String | Data | List | Result | Error | type -> type | (type)

Inside the REPL, there is also a top-level binding form:

repl ::= let name = expr

Bare REPL bindings persist across later entries, so you can write:

let x = 42
x + 1

Expressions are checked as Type ! Effects. Plain numeric literals now default to Rational, so 1, 1.25, 1 + 2, and 5 / 2 are all rational values unless you explicitly cast them. Division carries {Error} implicitly. readFile is a builtin with type String -> String ! {IO, Error}. readFileNickel is a builtin with type String -> Data ! {IO, Error}. parseNickel is a builtin with type String -> Data ! {Error} and is backed by the real Nickel Rust crate. Nickel source is evaluated on the host side and converted back into mlang Data. Parsed data inspection is available through:

  • httpGet : String -> String ! {IO, Error}

  • parseJson : String -> Data ! {Error}

  • get : Data -> String -> Data ! {Error}

  • at : Data -> Rational -> Data ! {Error}

  • asInt : Data -> Int ! {Error}

  • asDecimal : Data -> Decimal ! {Error}

  • asString : Data -> String ! {Error}

  • asBool : Data -> Bool ! {Error}

  • toInt : Rational -> Int ! {Error}

  • toDecimal : Rational -> Decimal ! {Error}

  • toJson : Data -> String

  • toNickel : Data -> String

  • rationalStability : Rational -> Stability

  • decimalStability : Rational -> Stability

  • sqrt : Decimal -> LazyReal ! {Error}

  • pi : LazyReal

  • lazyRationalStability : LazyReal -> Stability

  • lazyDecimalStability : LazyReal -> Stability

  • approx : LazyReal -> Rational -> Rational ! {Error}

  • bounds : LazyReal -> Rational -> Data ! {Error}

httpGet is implemented through a thin Rust host library plus a small C shim for Lean FFI. The Rust layer uses a real HTTP client crate rather than spawning an external tool. try ... with name => ... handles Error, binds the caught error as an Error value, and removes Error from the resulting effect set when recovered locally.

The host-side Nickel bridge currently maps evaluated Nickel values back into mlang data for:

  • null
  • booleans
  • integers
  • decimals
  • strings
  • arrays
  • records

Exact rational values are supported in native literals and in Data values coming from JSON or Nickel.

Native Data literals are also supported directly in mlang:

{ answer = 42, flags = [true, false], note = "ok", empty = null }

Fields and array elements may be Rational, Bool, String, or existing Data expressions.

LazyReal represents process-like numeric values that are observed through refinement. The current implementation supports decimal input to sqrt, while approx and bounds observe lazies as rationals:

let r = sqrt 2.0
approx r 4
bounds r 4
approx pi 4
toDecimal (approx pi 4)

Ordinary arithmetic lifts over LazyReal, so expressions like sqrt 2.0 + 3.0 stay lazy until observed with approx or bounds.

Stability observations classify how a number settles under rational and decimal observation:

rationalStability (1 / 3)      -- Immediate
decimalStability (1 / 3)       -- Periodic
lazyRationalStability pi       -- Refining
lazyDecimalStability (sqrt 2)  -- Refining

pmap name in expr => body expects expr to evaluate to a native Data array. It evaluates body concurrently for each element bound to name, preserves input order, and returns a native List of native Result values instead of failing the whole traversal.

There is also a thread-first form that desugars statically to nested application:

-> "examples/sample.ncl" readFileNickel (get "answer")

Parallel collection mapping example:

pmap x in parseNickel "[{ answer = 1 }, { missing = 2 }, { answer = 3 }]"
  => get x "answer"

Files and REPL entries may contain multiple expressions separated by ;. Each expression is parsed, typechecked, evaluated, and printed in order. REPL-specific bare let bindings persist across later entries. devenv shell -- run now launches a Rust rustyline frontend for the REPL, while the Lean binary still handles evaluation. Use :quit to exit the REPL.

The checked-in sample program is examples/samples.mlg, which now contains the old demo coverage as semicolon-separated expressions, including file IO, Nickel parsing, threading, pmap, local error handling, and an HTTP fetch. It reads from examples/sample.ncl.

A separate HTTP example is examples/http.mlg.

Next steps#

  • add recursive functions
  • add algebraic data types and pattern matching
  • compile to a bytecode VM or another backend