Skip to content

Latest commit

 

History

History
400 lines (268 loc) · 14.1 KB

File metadata and controls

400 lines (268 loc) · 14.1 KB

Mirash

Mirash is a lazy, weak and dynamically typed functional programming language for shell. Mirash's syntax is heavily inspired off the Haskell family of languages, while dropping the safety of typing for use in a shell.

Why dropping typing?

While not completely allergic to typing, Mirash is designed to work with values of which we don't know its type. It is factually true that this issue can be solved by the introduction of a super object or by treating such values as strings, but it was a design decision to opt for a model that has an historical and practical use with the things we find essential in a shell.

Language

Mirash is defined as the following grammar:

(* ─── Lexical ─────────────────────────────────────────── *)

lower        ::= [a-z]
upper        ::= [A-Z]
letter       ::= [a-zA-Z]
digit        ::= [0-9]
op-char      ::= [+\-*/<>=!&|#$%^~?:.\\]   (* @ removed, used for stream-arg *)

ident        ::= lower (letter | digit | "'" | '_')*
constructor  ::= upper (letter | digit | "'")*

(* Mixfix operator names — underscores mark argument holes *)
(* e.g. if_then_else_  _|_  _+_  _!  ¬_  open_with_ *)
name-part    ::= (letter | digit | op-char)+   (* one chunk, no underscores *)
mixfix-op    ::= '_'? name-part ('_' name-part)* '_'?

number-lit   ::= '-'? digit+ ('.' digit+)?
string-lit   ::= '"' [^"]* '"'
             |   '"""' .*? '"""'               (* multiline *)
field-assign ::= ident '=' expr
record-lit   ::= '{' (field-assign (',' field-assign)*)? '}'
literal      ::= number-lit | string-lit | record-lit

comment      ::= '--' [^\n]* '\n'
             |   '{-' .*? '-}'                 (* nestable *)

(* ─── Layout ──────────────────────────────────────────── *)

(* Like Haskell, braces and semicolons may be omitted.
   The lexer inserts implicit '{', ';', '}' based on
   indentation after 'where', 'let', 'of', and 'do'.
   Explicit braces and semicolons override layout. *)

(* ─── Patterns ────────────────────────────────────────── *)

pat          ::= '_'
             |   ident
             |   ident '@' pat                 (* as-pattern *)
             |   constructor pat*
             |   literal
             |   '[' (pat (',' pat)*)? ']'
             |   '(' pat ')'

(* ─── Definitions ─────────────────────────────────────── *)

rhs          ::= '=' expr
             |   ('|' expr '=' expr)+

where-clause ::= 'where' '{' def (';' def)* '}'

def          ::= ident pat* rhs where-clause?
             |   mixfix-op pat* rhs where-clause?

(* ─── Data declarations ───────────────────────────────── *)

field-decl   ::= ident (',' ident)*
data-con     ::= constructor '{' field-decl '}'
             |   constructor '_'*

data-decl    ::= 'data' data-con ('|' data-con)*

(* ─── Class & Instance declarations ───────────────────── *)

class-decl   ::= 'class' (ident | mixfix-op) digit+

dispatch-tag ::= 'String' | 'Number' | 'Record'
             |   'List'   | 'Stream' | constructor

instance-decl ::= 'instance' (ident | mixfix-op) dispatch-tag
                  'where' '{' def (';' def)* '}'

(* ─── Fixity ───────────────────────────────────────────── *)

(* precedence applies to the whole mixfix-op pattern *)
fixity-decl  ::= ('infixl' | 'infixr' | 'infix' | 'prefix' | 'postfix')
                 digit+ mixfix-op+

(* ─── Do notation ─────────────────────────────────────── *)

do-stmt      ::= pat '<-' expr
             |   'let' '{' def (';' def)* '}'
             |   expr

do-expr      ::= 'do' '{' do-stmt (';' do-stmt)* '}'

(* ─── Expressions ─────────────────────────────────────── *)

(* The Pratt parser operates on a flat sequence of expr-atoms.
   It consults the fixity table to resolve mixfix operators,
   function application, and precedence uniformly.
   Ambiguous parses are a compile-time error.
   e.g. if_then_else_ e1 e2 e3 parses as: if e1 then e2 else e3 *)

stream-arg   ::= '@' atom

qual         ::= pat '<-' expr
             |   expr

atom         ::= ident
             |   constructor
             |   literal
             |   '(' expr ')'
             |   '[' ']'
             |   '[' expr (',' expr)* ']'
             |   '[' expr '..' expr? ']'
             |   '[' expr '|' qual (',' qual)* ']'
             |   do-expr

alt          ::= pat ('->' expr | ('|' expr '->' expr)+)

expr-atom    ::= atom | name-part | stream-arg

expr         ::= '\' pat+ '->' expr
             |   'let' '{' def (';' def)* '}' 'in' expr
             |   'case' expr 'of' '{' alt (';' alt)* '}'
             |   expr-atom+

(* ─── Imports ─────────────────────────────────────────── *)

import-item  ::= ident | constructor | '(' mixfix-op ')'
module-path  ::= constructor ('.' constructor)*

import-decl  ::= 'import' module-path
                 ('(' import-item (',' import-item)* ')')?
             |   'import' module-path 'as' constructor
             |   'import' 'qualified' module-path
                 ('as' constructor)?
                 ('(' import-item (',' import-item)* ')')?

(* ─── Top-level ────────────────────────────────────────── *)

export       ::= ident | constructor | '(' mixfix-op ')'
module-decl  ::= 'module' module-path
                 ('(' export (',' export)* ')')?

top-decl     ::= module-decl
             |   import-decl
             |   fixity-decl
             |   data-decl
             |   class-decl
             |   instance-decl
             |   def

program      ::= top-decl*

Mixfix resolution: the Pratt parser tokenizes name-part chunks and _ holes. When it encounters a name-part it checks the fixity table for all registered mixfix-op patterns that begin with or contain that part at the current precedence level, then greedily consumes argument expressions to fill holes. Function application binds tighter than any operator. Ambiguous parses are a compile-time error.

Mirash Core - Semantics

We define the core semantics as follows:

(* =================================================================
   Judgment: Env : e ~> Env' : v
   Env is a heap mapping variables to unevaluated expressions (thunks)
   v is a value in WHNF
   ================================================================= *)


(* --- Values ----------------------------------------------------- *)

v ::= n                        -- number  (decimal)
    | s                        -- string
    | r                        -- record  { f = v, ... }
    | \x.e                     -- lambda
    | C v*                     -- positional constructor (saturated)
    | C {f = v}*               -- record constructor (saturated)


(* --- Tags ------------------------------------------------------- *)

t ::= Number | String | Record | List | Stream | C


(* --- Core Reduction --------------------------------------------- *)


-------------------------------------------------------------------- (Lit)
Env : v ~> Env : v


Env[x -> e] : e ~> Env'[x -> e] : v
-------------------------------------------------------------------- (Var)
Env[x -> e] : x ~> Env'[x -> v] : v


Env : e' ~> Env' : \x.e      Env'[x -> e''] : e ~> O : v
-------------------------------------------------------------------- (App)
Env : e' e'' ~> O : v


Env : e' ~> Env' : f      not (f in \)
Env'      : e_1 ~> Env_1 : v_1
Env_1     : e_2 ~> Env_2 : v_2
  ...
Env_(n-1) : e_n ~> Env_n : v_n
-------------------------------------------------------------------- (App-FFI)
Env : e' e_1 ... e_n ~> Env_n : ffi(f, v_1, ..., v_n)


Env : e' ~> Env' : f      not (f in \)      Env' : s ~> Env'' : v_s
-------------------------------------------------------------------- (App-FFI-Stream)
Env : e' @s ~> Env'' : ffi(f, stdin=v_s)


Env[x -> e'] : e'' ~> Env' : v
-------------------------------------------------------------------- (Let)
Env : let x = e' in e'' ~> Env' : v


Env : e ~> Env' : C v_1 ... v_n
Env'[x_1 -> v_1, ..., x_n -> v_n] : e_k ~> O : v
-------------------------------------------------------------------- (Case-Con)
Env : case e of { ... ; C x_1 ... x_n -> e_k ; ... } ~> O : v


Env : e ~> Env' : C {f_1 = v_1, ..., f_n = v_n}
Env'[x_1 -> v_1, ..., x_n -> v_n] : e_k ~> O : v
-------------------------------------------------------------------- (Case-Rec)
Env : case e of { ... ; C {f_1 = x_1, ..., f_n = x_n} -> e_k ; ... } ~> O : v


Env : e ~> Env' : l      l = l_k      Env' : e_k ~> O : v
-------------------------------------------------------------------- (Case-Lit)
Env : case e of { ... ; l_k -> e_k ; ... } ~> O : v


-------------------------------------------------------------------- (Case-Wild)
Env : case v of { _ -> e ; ... } ~> Env : e


Env : e ~> Env' : v      v does not match p_k
Env' : case v of { p_(k+1) -> e_(k+1) ; ... } ~> O : u
-------------------------------------------------------------------- (Case-Skip)
Env : case e of { p_k -> e_k ; p_(k+1) -> e_(k+1) ; ... } ~> O : u


-------------------------------------------------------------------- (Case-Exhaust)
Env : case v of {} ~> runtime error


(* --- Coercions  v ~>_t v' --------------------------------------- *)
(*   Fire at force sites when tag(v) /= demanded t                 *)


-------------------------------------------------------------------- (Co-Num-Str)
n ~>_String show(n)


parse(s) = n
-------------------------------------------------------------------- (Co-Str-Num)
s ~>_Number n


parse(s) fails
-------------------------------------------------------------------- (Co-Str-Num-Fail)
s ~>_Number runtime error


-------------------------------------------------------------------- (Co-List-Stream)
[v_1, ..., v_n] ~>_Stream Cons v_1 (... (Cons v_n Nil))


-------------------------------------------------------------------- (Co-Str-Stream)
s ~>_Stream Cons l_1 (... (Cons l_n Nil))       (l_i are lines of s)


s ~>_String s'      s' ~>_Stream stream
-------------------------------------------------------------------- (Co-Num-Stream)
n ~>_Stream stream


-------------------------------------------------------------------- (Co-Num-List)
n ~>_List [n]


-------------------------------------------------------------------- (Co-Str-List)
s ~>_List [l_1, ..., l_n]                       (l_i are lines of s)


(* List coercions take the head element only; tail is discarded *)

v_1 ~>_Number n
-------------------------------------------------------------------- (Co-List-Num)
[v_1, ..., v_n] ~>_Number n


v_1 ~>_String s
-------------------------------------------------------------------- (Co-List-Str)
[v_1, ..., v_n] ~>_String s


-------------------------------------------------------------------- (Co-List-Num-Empty)
[] ~>_Number runtime error


-------------------------------------------------------------------- (Co-List-Str-Empty)
[] ~>_String runtime error


(* Record coercion: a record of constructor A coerces only to
   Record A — no cross-constructor coercion is defined.
   Non-record values have no coercion path to Record. *)

(* Stream-to-List coercion is intentionally absent;
   streams may be infinite. *)


(* --- Coerce meta-rule ------------------------------------------- *)
(*   Wires coercions into main semantics at any force site         *)

Env : e ~> Env' : v      tag(v) /= t      v ~>_t v'
-------------------------------------------------------------------- (Coerce)
Env : e ~> Env' : v'

Syntactic Sugar

Expressions for the core language are well-defined. The first pass is responsible for desugaring a well-formed Mirash program into a well-defined Mirash Core program. Thus, all non-essential expressions like mixfix operators or do notation are purely represented as standard functions and values.

Desugaring rules:

  • Mixfix operators desugar into plain function application. if e1 then e2 else e3 becomes if_then_else_ e1 e2 e3.
  • Do notation desugars <- binds into flatMap calls:
    do { x <- e1 ; e2 }   ~>   flatMap e1 (\x -> e2)
    do { e1 ; e2 }         ~>   flatMap e1 (\_ -> e2)
    do { let { d } ; e }   ~>   let { d } in e
    do { e }                ~>   e
    
  • Guards in definitions desugar into nested case on boolean values.
  • List comprehensions desugar into nested flatMap and filter calls.

Unbound functions are executed by the runtime as eager FFI calls to shell commands.

Module resolution

Modules are resolved first from MIRASH_MODULE_DIRECTORY, then from the local directory. The first match wins; if a local module shares a name with one in the module directory, the module directory takes priority. Module shadowing is possible. We encourage namespacing modules with dots, like in Haskell:

module Library.A.Component

import Core.B.Component (function)

Shell execution

Unbound function calls are lifted into shell commands. While Mirash is lazy, FFI calls are eager — the external program runs to completion when forced, since there is no way to impose lazy evaluation on external processes.

Given a definition such as x = curl "https://example.com", x is a Stream that, when evaluated, will either coerce into a string or operate as a Stream.

Streams are primitives given by the runtime, injected over the core language. They work on outputs and inputs from the real world and expose the following primitives:

codeOfStream  -- returns the exit code of a stream
map           -- standard map
flatMap       -- standard flatMap
filter        -- standard filter
pure          -- wraps a value into a stream

Arguments that accept piped input are supplied with @: grep "<pattern>" @input.

Streams are implemented directly in the Mirash Runtime.