Syntax
Principles:
-
Minimal Syntax Base
- The basic syntax is just
varvar,defdef,looploop&breakbreak, andclassclass. The rest of all syntax are either syntax variants or sugar of “operational trait”.
- The basic syntax is just
-
Composition-first polymorphism.
- no inheritance among classes.
- a class must implement a trait only by C++ concept.
- simple subtyping: arrays, dictionaries, traits and functions derives subtyping simply by their members.
valvalandimportimportaliasing. You can steal methods from otherwhere simply by aliasing:
class/trait T {import (method) from OtherTrait;val method = OtherTrait.method;}class/trait T {import (method) from OtherTrait;val method = OtherTrait.method;} -
Consistent syntax.
- Doesn’t distinguish macro (pre-compile-time) functions, type (compile-time) functions and value (runtime) functions.
- Consistent enum/pattern matcing syntax:
class Option {case Nonecase Some(T)}opt match {case None => ..,case Some(_) => ..,}def checkOption(t: Option(T)) = /* could omit t match */ {case None => ..,case Some(_) => ..,}class Option {case Nonecase Some(T)}opt match {case None => ..,case Some(_) => ..,}def checkOption(t: Option(T)) = /* could omit t match */ {case None => ..,case Some(_) => ..,}- Consistent declaring syntax:
val Pair = False;type Pair = False;def Pair = False;val Pair = (U, V) => (U, V);type Pair(U, V) = (U, V);def Pair(U, V) = (U, V);def Pair(U, V) = class { val first: U; val second: V; };class Pair(U, V) { val first: U; val second: V; };val Pair = False;type Pair = False;def Pair = False;val Pair = (U, V) => (U, V);type Pair(U, V) = (U, V);def Pair(U, V) = (U, V);def Pair(U, V) = class { val first: U; val second: V; };class Pair(U, V) { val first: U; val second: V; };- Consistent constructing syntax among array, dict, and arguments.
val arr = (1, 2, 3);val dict = (key: "value");val someCall = someFunc(1, 2, 3, key: "value");val arr = (1, 2, 3);val dict = (key: "value");val someCall = someFunc(1, 2, 3, key: "value");- Consistent applying syntax among destructing, function calls, and import.
val (a, b: c) = Args(1, b: "2");someCall(1, b: "2");import (a, b: c) from std.some.mod;val (a, b: c) = Args(1, b: "2");someCall(1, b: "2");import (a, b: c) from std.some.mod; -
Modular design and C++ interoperability.
- visibililty for faster compilation and safe API header.
- painlessly reusing any C++ code by
autoautomagics. - C++ side code can use cosmo functions by imcluding API headers.
External types can be handled by builtin externalexternal function:
def Vec(Ty: Type) = std.cpp.ty(std.vector(Ty));
def Vec(Ty: Type) = std.cpp.ty(std.vector(Ty));
template <typename Ty>using Vec = std::vector<Ty>;
template <typename Ty>using Vec = std::vector<Ty>;
Function body can be a type:
def Source /* inferred as : Type */ = class { val data = Vec(u8)}def Pair(Lhs: Type, Rhs: Type) /* inferred as : Type */ = (Lhs, Rhs);
def Source /* inferred as : Type */ = class { val data = Vec(u8)}def Pair(Lhs: Type, Rhs: Type) /* inferred as : Type */ = (Lhs, Rhs);
class Source { Vec<u8> data;};template <typename Lhs, typename Rhs>using Pair = std::tuple<Lhs, Rhs>;
class Source { Vec<u8> data;};template <typename Lhs, typename Rhs>using Pair = std::tuple<Lhs, Rhs>;
Inference and specialization for implicit parameters:
// implicit type parameters are inferred automaticallyval identity(implicit T: Type)(val v: T) = v;// Since they are currying functions, grouped braces are not necessaryval identity(implicit T: Type, val v: T) = v;// partial specializationval identityU8 = identity(implicit u8);
// implicit type parameters are inferred automaticallyval identity(implicit T: Type)(val v: T) = v;// Since they are currying functions, grouped braces are not necessaryval identity(implicit T: Type, val v: T) = v;// partial specializationval identityU8 = identity(implicit u8);
template <typename T>T identity(T v) { return v;}constexpr auto identityU8 = identity<u8>;
template <typename T>T identity(T v) { return v;}constexpr auto identityU8 = identity<u8>;
Values are types at level 0. Types are types at level 1. Types at higher levels are usually constructed by functions.
In particular, constant values can be lifted and evaluated at compile-time:
val lift(implicit T: Type)(val v: T) = Type;val True = lift(true);// orval False = Type(false);
val lift(implicit T: Type)(val v: T) = Type;val True = lift(true);// orval False = Type(false);
template <typename T>struct lift { using type = T;};using True = std::true_type;using False = std::false_type;
template <typename T>struct lift { using type = T;};using True = std::true_type;using False = std::false_type;
Constructing a type from a type expression:
def RoundBits(T: Type) = if (IsUnsigned(T)) { u64} else { i64}
def RoundBits(T: Type) = if (IsUnsigned(T)) { u64} else { i64}
template <typename T>using RoundBits = std::conditional_t<IsUnsigned<T>::value, u64, i64>;
template <typename T>using RoundBits = std::conditional_t<IsUnsigned<T>::value, u64, i64>;
You can view the syntax values as having negative level, but we don’t model them with that sense. The hygiene macros are just evaluated before any other expressions and cannot be evaluated in latter stages:
def path(implicit S: syntax.Expr, self, field: S) = S match { case syntax.Expr.Apply(syntax.Expr.Self, field) | syntax.Expr.Ident(field) => std.code { self.apply(field) } case syntax.Expr.Apply(field, index) => std.code { self.apply(field).andThen(_.value.apply(index)) } case syntax.Expr.Select(target, field) => std.code { self.path(target) } case _ => panic("path is not valid");}
def path(implicit S: syntax.Expr, self, field: S) = S match { case syntax.Expr.Apply(syntax.Expr.Self, field) | syntax.Expr.Ident(field) => std.code { self.apply(field) } case syntax.Expr.Apply(field, index) => std.code { self.apply(field).andThen(_.value.apply(index)) } case syntax.Expr.Select(target, field) => std.code { self.path(target) } case _ => panic("path is not valid");}
/* no corresponding code */
/* no corresponding code */
Then you can use it in the following way:
val field = j.path { field };val field = j.path { self.field };val first = j.path { self.field(0) };val selfValue = j.path { self("self") };
val field = j.path { field };val field = j.path { self.field };val first = j.path { self.field(0) };val selfValue = j.path { self("self") };
auto field = j.apply("field").val;auto field = j.apply("field").val;auto first = j.apply("field").val.apply(0).val;auto selfValue = j.apply("self").val;
auto field = j.apply("field").val;auto field = j.apply("field").val;auto first = j.apply("field").val.apply(0).val;auto selfValue = j.apply("self").val;
valval and varvar are used to declare variables. valval is immutable, while varvar is mutable.
val EnableLogging: Type = True;
val EnableLogging: Type = True;
constexpr auto EnableLogging = true;
constexpr auto EnableLogging = true;
var count = 0;count += 1;
var count = 0;count += 1;
int count = 0;count += 1;
int count = 0;count += 1;
The difference between valval and defdef is that valval is evaluated at the point of declaration, while defdef is evaluated at the point of use:
val EnableLogging = True;/* may fail but won't cause compile error unless you use it */def EnableLogging = Never;
val EnableLogging = True;/* may fail but won't cause compile error unless you use it */def EnableLogging = Never;
/* no corresponding code */
/* no corresponding code */
typetype (type aliasing) items defines types above level 0 (cannot be a runtime values):
type EnableLogging = True;
type EnableLogging = True;
is equivalent to:
val EnableLogging: Type = True;
val EnableLogging: Type = True;
def IsUnsigned(T: Type) = { T == u8 or T == u16 or T == u32 or T == u64}
def IsUnsigned(T: Type) = { T == u8 or T == u16 or T == u32 or T == u64}
They are just functions that return TrueTrue or FalseFalse.
Using assertassert anywhere in the function body will cause a compile-time error if the condition is not satisfied:
def MustUnsigned(T: Type) = { assert(IsUnsigned(T)); True}
def MustUnsigned(T: Type) = { assert(IsUnsigned(T)); True}
template <typename T>struct MustUnsigned { static_assert(IsUnsigned<T>::value); using type = std::true_type;};
template <typename T>struct MustUnsigned { static_assert(IsUnsigned<T>::value); using type = std::true_type;};
A string literal is wrapped either by one "" or by at least three "".
val string = "x";val string = """x""";
val string = "x";val string = """x""";
template literals are strings prefixed with a “template prefix function”:
val string = s"x $variable y";val string = s"""x $variable y""";val string = s"x ${variable}_y";val string = s"""x ${variable}_y""";
val string = s"x $variable y";val string = s"""x $variable y""";val string = s"x ${variable}_y";val string = s"""x ${variable}_y""";
The signature of ss is:
def s(implicit FmtArgs: Type, f: String, args: FmtArgs): String;
def s(implicit FmtArgs: Type, f: String, args: FmtArgs): String;
A developer can define their own template prefix function:
def myFmt(implicit FmtArgs: Type, f: String, args: FmtArgs): FmtResult(FmtArgs);val string = myFmt"x $variable y";
def myFmt(implicit FmtArgs: Type, f: String, args: FmtArgs): FmtResult(FmtArgs);val string = myFmt"x $variable y";
Arrays and Dictionaries can be constructed by ()():
val arr = (1, 2, 3);val dict = (key: "value");
val arr = (1, 2, 3);val dict = (key: "value");
auto arr = std::array{1, 2, 3};auto dict = std::map<std::string, std::string>{{"key", "value"}};
auto arr = std::array{1, 2, 3};auto dict = std::map<std::string, std::string>{{"key", "value"}};
In actual, they have same shape as function calls:
val arr = Array(1, 2, 3);val dict = Dict(key: "value");val someCall = someFunc(1, 2, 3, key: "value");
val arr = Array(1, 2, 3);val dict = Dict(key: "value");val someCall = someFunc(1, 2, 3, key: "value");
As alternative syntax, you can use ->-> to construct dictionaries:
val dict = (key -> "value");val (key -> value) = dict;
val dict = (key -> "value");val (key -> value) = dict;
As alternative syntax, you can use == to call functions:
val someCall = someFunc(1, 2, 3, key = "value");
val someCall = someFunc(1, 2, 3, key = "value");
You can spread an array, a dictionary, or an argument instance:
val arr = (1, 2, 3);val dict = (key: "value");val args = Args(1, 2, 3, key: "value");val someCall = someFunc(..arr, ..dict, ..args);
val arr = (1, 2, 3);val dict = (key: "value");val args = Args(1, 2, 3, key: "value");val someCall = someFunc(..arr, ..dict, ..args);
Note: the shape of spreaded values must be determined at compile-time.
Putting array or dictionary on left side of == will destruct it:
val (a, b, c) = (1, 2, 3);val (key = value) = (key = "value");
val (a, b, c) = (1, 2, 3);val (key = value) = (key = "value");
auto [a, b, c] = std::make_tuple(1, 2, 3);auto [key, value] = std::map<std::string, std::string>{{"key", "value"}};
auto [a, b, c] = std::make_tuple(1, 2, 3);auto [key, value] = std::map<std::string, std::string>{{"key", "value"}};
A value can be matched by cases:
val hasValue = mayT match { case None => false case Some(_) => true}
val hasValue = mayT match { case None => false case Some(_) => true}
Conceptually, an item declaration only exhibit the way of using the item, and an item definition contains implementation code.
Conceptually, a declaration or a definition will be placed in either a generated public C++ header (*.h*.h api files), a generated private C++ header (*.h*.h private files), or a generated C++ source file (*.cc*.cc files).
four visibililty:
- (default): the declaration tends to be generated in
.cc.ccfiles, but can be lifted to*.h*.hprivate files as well. - private: the declaration must be generated in
.cc.ccfiles. - pub: the declaration must be generated in
.h.hapi files. @inline pub@inline pub: both declaration and definition are generated in.h.hapi files.
Note: @inline@inline is not a visibililty feature, but a compiler flag.
Only explicit pubpub items can be ensured to be seen by C++ side code.
pub def foreignCallable();
pub def foreignCallable();
import (value, parse) from T;
import (value, parse) from T;
is equivalent to:
val (value, parse) = import(T);
val (value, parse) = import(T);
using value = T::value;using parse = T::parse;
using value = T::value;using parse = T::parse;
C++ headers are not imported as anonymous header-only dependencies. A C++ import must bind an explicit local alias to a C++ namespace and a c++/<header>c++/<header> source:
import std as cstd from "c++/vector"
import std as cstd from "c++/vector"
This introduces only the local alias cstdcstd. The original C++ namespace name stdstd is not made visible as a Cosmo binding.
Compatible imports with the same alias and namespace merge their header inputs:
import std as cstd from "c++/vector"import std as cstd from "c++/string"
import std as cstd from "c++/vector"import std as cstd from "c++/string"
The merged alias targets ::std::std and contributes both <vector><vector> and <string><string> to backend validation. Reusing the same alias for a different namespace is an error.
The header-only form is unsupported:
import "c++/vector" // error: C++ imports require an explicit namespace alias
import "c++/vector" // error: C++ imports require an explicit namespace alias
Qualified names resolve through the alias:
type CppVector[T] = cstd::vector[T]
type CppVector[T] = cstd::vector[T]
The compiler validates the suffix against the alias namespace and bounded header set through cosmo-clang-syscosmo-clang-sys before treating the C++ symbol as a backend input.
import std.json;
import std.json;
#include <cosmo/json/index.hpp> // generated
#include <cosmo/json/index.hpp> // generated
is equivalent to:
import "@std/json" as json;
import "@std/json" as json;
#include <cosmo/std/json/index.hpp> // generated
#include <cosmo/std/json/index.hpp> // generated
To import some specific items inside project
import self.a.b.c;
import self.a.b.c;
#include <myOrg/project/a/b/c/index.hpp> // generated
#include <myOrg/project/a/b/c/index.hpp> // generated
is equivalent to:
import (a: (b: c)) from "@myOrg/project";
import (a: (b: c)) from "@myOrg/project";
#include <myOrg/project/a/b/c/index.hpp> // generated
#include <myOrg/project/a/b/c/index.hpp> // generated
pub import (value, parse) from T;
pub import (value, parse) from T;
For an item a.b.ca.b.c in @org/project@org/project.
// import @org/projectnamespace org {namespace project {// import a.b.cnamespace a::b {namespace details {item c; // generated code will be here}// @external(cpp)// def name(implicit T, val: T)template<T>auto c(T val, Extras extrasIfAny) { details(extratsIfAny.ctx, val);}}}}
// import @org/projectnamespace org {namespace project {// import a.b.cnamespace a::b {namespace details {item c; // generated code will be here}// @external(cpp)// def name(implicit T, val: T)template<T>auto c(T val, Extras extrasIfAny) { details(extratsIfAny.ctx, val);}}}}
Normal classes can have val/var/def items as fields:
class Nat { val data = u64 var count = 0 def to_int(self) = data}
class Nat { val data = u64 var count = 0 def to_int(self) = data}
class Nat { uint64_t data; int count; uint64_t to_int() { return data; }};
class Nat { uint64_t data; int count; uint64_t to_int() { return data; }};
def items without selfself are static methods:
class Nat { .. def from_int(n: u64) = Nat(n)}
class Nat { .. def from_int(n: u64) = Nat(n)}
class Nat { .. static Nat from_int(uint64_t n) { return Nat(n); }};
class Nat { .. static Nat from_int(uint64_t n) { return Nat(n); }};
Enum classes are constructed by cases:
class Nat { case Zero case Succ(Nat)}
class Nat { case Zero case Succ(Nat)}
class Nat { std::variant<Zero, Succ> data;};
class Nat { std::variant<Zero, Succ> data;};
It has same shape as pattern matching:
val hasSucc = nat match { case Zero => false case Succ(Nat) => true}
val hasSucc = nat match { case Zero => false case Succ(Nat) => true}
Compared with Option(T).mapOption(T).map:
val hasValue = mayT.map { case None => false case Some(_) => true}
val hasValue = mayT.map { case None => false case Some(_) => true}
Nat-Add Example:
def add(A: Nat, B: Nat): Nat = A match { case Zero => B case Succ(B) => Succ(Add(A, B))}
def add(A: Nat, B: Nat): Nat = A match { case Zero => B case Succ(B) => Succ(Add(A, B))}
Nat add(Nat A, Nat B) { switch (A.data.index()) { case 0: return B; case 1: return Nat::Succ(add(std::get<1>(A.data)._0, B)); }}
Nat add(Nat A, Nat B) { switch (A.data.index()) { case 0: return B; case 1: return Nat::Succ(add(std::get<1>(A.data)._0, B)); }}
Common methods can be specified in the default branch:
class Nat { .. case _ => { def to_int(self) = self match { case Zero => 0 case Succ(n) => 1 + n.to_int() } }}
class Nat { .. case _ => { def to_int(self) = self match { case Zero => 0 case Succ(n) => 1 + n.to_int() } }}
Traits are classes containing unimplemented methods, while you can provide default impls:
trait Unsigned(T: Type) { assert(IsUnsigned(T.value)); def asUint64(self): u64 = staticCast(u64, self.value);}
trait Unsigned(T: Type) { assert(IsUnsigned(T.value)); def asUint64(self): u64 = staticCast(u64, self.value);}
struct UnsignedConcept { virtual uint64_t asUint64() = 0; virtual ~UnsignedConcept() = default;};template <typename T>struct UnsignedModel: public UnsignedConcept { T& self; UnsignedModel(T& self) : self(self) { static_assert(IsUnsigned<T::ValueT>::value); } static_assert(IsUnsigned<T>::value); uint64_t asUint64() override { return static_cast<uint64_t>(self.value); }};
struct UnsignedConcept { virtual uint64_t asUint64() = 0; virtual ~UnsignedConcept() = default;};template <typename T>struct UnsignedModel: public UnsignedConcept { T& self; UnsignedModel(T& self) : self(self) { static_assert(IsUnsigned<T::ValueT>::value); } static_assert(IsUnsigned<T>::value); uint64_t asUint64() override { return static_cast<uint64_t>(self.value); }};
You can implement a trait for a class:
impl Unsigned(u8) for Nat { def asUint64(self) = self.to_int()}
impl Unsigned(u8) for Nat { def asUint64(self) = self.to_int()}
struct NatUnsignedModel: public UnsignedModel<Nat> { NatUnsignedModel(Nat& self) : UnsignedModel<Nat>(self) {} uint64_t asUint64() override { return self.to_int(); }};
struct NatUnsignedModel: public UnsignedModel<Nat> { NatUnsignedModel(Nat& self) : UnsignedModel<Nat>(self) {} uint64_t asUint64() override { return self.to_int(); }};
If a class implements multiple traits with the same method name, you can disambiguate them by specifying the trait:
(nat as Unsigned(u8)).asUint64();
(nat as Unsigned(u8)).asUint64();
NatUnsignedModel(nat).asUint64();
NatUnsignedModel(nat).asUint64();
There are two traits having a same method, however, you want to have a function receiving an object implemented both trait. You can mix a new trait by renaming the conflict method:
trait FromBoth(X: Type, Y: Type) { import (*, from: fromOutput) from From(X, Self); import (*, from: fromError) from From(Y, Self);}def erase(items: X or Y, eraser: FromBoth(X, Y)) = { items.map { case X => eraser.fromOutput(X) case Y => eraser.fromError(Y) }}
trait FromBoth(X: Type, Y: Type) { import (*, from: fromOutput) from From(X, Self); import (*, from: fromError) from From(Y, Self);}def erase(items: X or Y, eraser: FromBoth(X, Y)) = { items.map { case X => eraser.fromOutput(X) case Y => eraser.fromError(Y) }}
ifif expression:
if (enableLogging) { println("Logging enabled") }
if (enableLogging) { println("Logging enabled") }
if a ifif expression’s condition is a boolean type expression at level 1, it will be evaluated at compile-time:
type EnableLogging = True;if (EnableLogging) { println("Logging enabled") }
type EnableLogging = True;if (EnableLogging) { println("Logging enabled") }
if constexpr (EnableLogging) { println("Logging enabled"); }
if constexpr (EnableLogging) { println("Logging enabled"); }
val BoundInt = if (IsUnsigned(T)) { u64 } else { i64 }
val BoundInt = if (IsUnsigned(T)) { u64 } else { i64 }
using BoundInt = std::conditional_t<IsUnsigned<T>::value, u64, i64>;
using BoundInt = std::conditional_t<IsUnsigned<T>::value, u64, i64>;
forfor expression:
for (i in 0..10) { println(i) }
for (i in 0..10) { println(i) }
for (auto i = 0; i < 10; i++) { println(i); }
for (auto i = 0; i < 10; i++) { println(i); }
is equivalent to:
val i = 0loop { if (i >= 10) { break } println(i) i += 1}
val i = 0loop { if (i >= 10) { break } println(i) i += 1}
auto i = 0;for (;;) { if (i >= 10) { break; } println(i); i += 1;}
auto i = 0;for (;;) { if (i >= 10) { break; } println(i); i += 1;}
You can also uses breakbreak and continuecontinue in forfor and looploop.
def range(from: u64, to: u64) = for (i in from..to) yield i;
def range(from: u64, to: u64) = for (i in from..to) yield i;
Then you can use it in the following way:
for (i in range(0, 10)) { println(i) }
for (i in range(0, 10)) { println(i) }
is equivalent to:
range(0, 10).foreach { i => println(i) }
range(0, 10).foreach { i => println(i) }
The yielded function is a function (C++ class) implementing IntoIterIntoIter trait.
trait Iter[T] { def next(self): Option[T]}trait IntoIter[T] { def into_iter(self): Iter[T]}
trait Iter[T] { def next(self): Option[T]}trait IntoIter[T] { def into_iter(self): Iter[T]}
returnreturn is used to return a value from a function:
def ten(): u64 = return 10;
def ten(): u64 = return 10;
The body of for comprehension shares the “return scope” with the outer function:
def ten = for (i in 1..100000) { return i;}
def ten = for (i in 1..100000) { return i;}
This is not very clear. You can compared with the map function with that in rust:
def hasValue(v: Option[u64]): bool = { v.map { return true } false}
def hasValue(v: Option[u64]): bool = { v.map { return true } false}
is not equivalent to:
fn has_value(v: Option<u64>) -> bool { v.map(|_| return true); // warning: ignored value false}
fn has_value(v: Option<u64>) -> bool { v.map(|_| return true); // warning: ignored value false}
def parsed(src: str): Result = { json.Value.parse(src)?; Ok(())}
def parsed(src: str): Result = { json.Value.parse(src)?; Ok(())}
trait Try(implicit Residual: Type, implicit Output: Type) { import (from: from_output) from From(Output, Self); def branch(self): ControlFlow(Residual, Error);}
trait Try(implicit Residual: Type, implicit Output: Type) { import (from: from_output) from From(Output, Self); def branch(self): ControlFlow(Residual, Error);}
Result is a type implementing TryTry trait:
class Result(O: Type, E: Type) { case Ok(O) case Err(E)}
class Result(O: Type, E: Type) { case Ok(O) case Err(E)}
def parseJsonFile(fileName: String): Result = { json.Value.parse(throw readFile(fileName))}
def parseJsonFile(fileName: String): Result = { json.Value.parse(throw readFile(fileName))}
is equivalent to
def parseJsonFile(implicit ctx: HandleCtx(PromiseHandler), fileName: String): Result = { json.Value.parse(ctx.handle(implicit PromiseHandler, readFile(fileName)))}
def parseJsonFile(implicit ctx: HandleCtx(PromiseHandler), fileName: String): Result = { json.Value.parse(ctx.handle(implicit PromiseHandler, readFile(fileName)))}
Result<Json,String> parseJsonFile(ctx: HandleCtx, fileName: String) { json::Value::parse(ctx.handle(readFile(fileName)))}
Result<Json,String> parseJsonFile(ctx: HandleCtx, fileName: String) { json::Value::parse(ctx.handle(readFile(fileName)))}
Note: implicit ctximplicit ctx must be visible by parseJsonFileparseJsonFile syntactically, like the implicit variables in scala. For example:
def main() { std.handle(PromiseHandler, (promise) => { val condVar = Signal(); var result; std.cpp.async(std.cpp.launch.async) { result = executePromise(promise) condVar.signal(); } condVar.wait(); return result; }); parseJsonFile(filePath); // ok!}parseJsonFile(filePath); // error! promise handler is not registed in the lexical context!parseJsonFile(implicit ctx, filePath); // ok! you pass it explicitly...
def main() { std.handle(PromiseHandler, (promise) => { val condVar = Signal(); var result; std.cpp.async(std.cpp.launch.async) { result = executePromise(promise) condVar.signal(); } condVar.wait(); return result; }); parseJsonFile(filePath); // ok!}parseJsonFile(filePath); // error! promise handler is not registed in the lexical context!parseJsonFile(implicit ctx, filePath); // ok! you pass it explicitly...
handlers are traits implemented apply function:
trait PromiseHandler { def apply(implicit T: Type, promise: Promise(T)): T;}
trait PromiseHandler { def apply(implicit T: Type, promise: Promise(T)): T;}