///|
async fn main {
let files = []
@ArgParser.parse([], path => files.push(path), "", @env.args()[1:].to_array())
for file in files {
if !@fs.exists(file) {
abort("file not found: \{file}")
}
let result = @fs.read_file(file).text() |> process_file(name=file)
@fs.write_file(file, result, truncate=true)
let _ = @process.collect_output("moonfmt", ["-w", file])
println("written to \{file}")
}
}
///|
let jmop_pkg = "@Yoorkin/jmop"
///|
let jmop_optional = "\{jmop_pkg}.Optional"
///|
let jmop_object = "\{jmop_pkg}.Object"
///|
let jmop_nullable = "\{jmop_pkg}.Nullable"
///|
let jmop_convert_error = "\{jmop_pkg}.convert_error"
///|
fn process_file(content : String, name~ : String) -> String {
let content = remove_block_line(content)
let (impls, reports) = @parser.parse_string(content)
if !reports.is_empty() {
println(reports)
abort("failed to parse \{name}")
}
let directives = impls.map(extract_directive)
let patches = generate_patch(directives)
apply_patch0(content, patches)
}
///|
enum TypeAnnotation {
Constr(@syntax.LongIdent, Array[TypeAnnotation])
Option(TypeAnnotation)
Tuple(Array[TypeAnnotation])
Arrow(Array[TypeAnnotation], TypeAnnotation, err~ : ErrorTypeAnnoatation)
Object(@syntax.LongIdent)
} derive(ToJson)
///|
pub(all) enum ErrorTypeAnnoatation {
ErrorType(TypeAnnotation)
DefaultErrorType
NoErrorType
Noraise
MaybeError(TypeAnnotation)
} derive(ToJson)
///|
enum ParamDirective {
Positional(name~ : String, ty~ : TypeAnnotation)
Optional(name~ : String, ty~ : TypeAnnotation)
Labeled(name~ : String, ty~ : TypeAnnotation)
} derive(ToJson)
///|
pub fn ParamDirective::name(self : Self) -> String {
match self {
Positional(name~, ..) | Optional(name~, ..) | Labeled(name~, ..) => name
}
}
///|
fn process_error_type(ety : @syntax.ErrorType) -> ErrorTypeAnnoatation {
match ety {
ErrorType(ty~) => ErrorType(process_type_annotation(ty))
DefaultErrorType(..) => DefaultErrorType
NoErrorType => NoErrorType
Noraise(..) => Noraise
MaybeError(ty~) => MaybeError(process_type_annotation(ty))
}
}
///|
fn process_type_annotation(ty : @syntax.Type) -> TypeAnnotation {
match ty {
Option(ty~, ..) => Option(process_type_annotation(ty))
Name(constr_id~, tys~, ..) =>
Constr(constr_id.id, tys.map(process_type_annotation).to_array())
Tuple(tys~, ..) => Tuple(tys.map(process_type_annotation).to_array())
Arrow(args~, res~, err~, ..) =>
Arrow(
args.map(process_type_annotation).to_array(),
process_type_annotation(res),
err=process_error_type(err),
)
Object(constr_id) => Object(constr_id.id)
Any(_) => panic() // TODO: report error
}
}
///|
fn process_parameter(param : @syntax.Parameter) -> ParamDirective {
match param {
DiscardPositional(ty~, loc~) => {
ignore((ty, loc))
abort("discard parameter is not supported")
}
Positional(binder~, ty~) => {
guard ty is Some(ty) // TODO: handle error case
Positional(name=binder.name, ty=process_type_annotation(ty))
}
Labelled(binder~, ty~) => {
guard ty is Some(ty) // TODO: handle error case
Labeled(name=binder.name, ty=process_type_annotation(ty))
}
Optional(binder~, default~, ty~) => {
guard ty is Some(ty) // TODO: handle error case
ignore(default) //TODO: use default value
Optional(name=binder.name, ty=process_type_annotation(ty))
}
QuestionOptional(binder~, ty~) => {
guard ty is Some(ty) // TODO: handle error case
Optional(name=binder.name, ty=process_type_annotation(ty))
}
}
}
///|
enum Directive {
Type(name~ : String, hole~ : Location)
Constructor(
ty~ : String,
arguments~ : Map[String, ParamDirective],
throws~ : Array[String],
hole~ : Location,
source_loc~ : String?
)
Getter(
name~ : String,
ty~ : String,
result~ : TypeAnnotation,
throws~ : Array[String],
hole~ : Location,
source_loc~ : String?
)
Setter(
name~ : String,
ty~ : String,
value~ : ParamDirective?,
throws~ : Array[String],
hole~ : Location,
source_loc~ : String?
)
Function(
name~ : String,
ty~ : String,
arguments~ : Map[String, ParamDirective],
result~ : TypeAnnotation,
throws~ : Array[String],
hole~ : Location,
source_loc~ : String?
)
Method(
name~ : String,
ty~ : String,
arguments~ : Map[String, ParamDirective],
result~ : TypeAnnotation,
throws~ : Array[String],
hole~ : Location,
source_loc~ : String?
)
Generated(hole~ : Location)
} derive(ToJson)
///|
fn extract_directive(toplevel : @syntax.Impl) -> Directive {
let attrs = match toplevel {
TopTypeDef(t) => t.attrs
TopFuncDef(fun_decl~, ..) => fun_decl.attrs
_ => @list.empty()
}
let exprs = attrs.filter_map(attr => match attr.parsed {
Some(Apply({ qual: Some("webgen"), name }, props)) =>
Some((name, props, attr.loc))
Some(Ident({ qual: Some("webgen"), name })) =>
Some((name, @list.empty(), attr.loc))
_ => None
})
guard exprs is More(main, tail=rest) else {
abort("expected ty, constructor, getter, setter")
}
if main.0 == "generated" {
return Generated(hole=toplevel.loc())
}
let throws = []
let arguments = {}
let (name, ty, hole, result) = match toplevel {
TopFuncDef(
fun_decl={
name: { name, .. },
type_name: Some({ name: Ident(name=ty), .. }),
decl_params,
return_type,
..,
},
decl_body=DeclBody(expr~, ..),
..
) => {
let name = if main.0 is ("getter" | "setter") {
let trimed = if name.has_prefix("get_") || name.has_prefix("set_") {
try! name[4:]
} else if name.has_prefix("is_") {
try! name[3:]
} else {
name
}
snake_to_small_camel(trimed)
} else {
name
}
for param in decl_params.unwrap() {
let d = process_parameter(param)
arguments[d.name()] = d
}
let result = return_type.map(process_type_annotation)
(name, ty, expr.loc(), result)
}
TopTypeDef({ tycon, loc, .. }) => (tycon, tycon, loc, None)
_ => abort("\{toplevel.loc()}: missing type name or binder name")
}
fn filter_name_prop(props : List[@attribute.Prop]) {
let result = props.filter_map(fn(prop) {
match prop {
Labeled("name", String(v)) => Some(v)
_ => None
}
})
match result {
More(r, tail=Empty) => Some(r)
Empty => None
_ => panic() // report error
}
}
let name = if filter_name_prop(main.1) is Some(v) { v } else { name }
for expr in rest {
let (name, props, loc) = expr
match name {
"throws" =>
for prop in props {
match prop {
Expr(String(s)) => throws.push(s)
_ => abort("\{loc}: invalid throws argument")
}
}
_ => abort("\{loc}: unknown webgen attribute: \{name}")
}
}
// TODO: better error handling
let source_loc = if arguments.contains("loc") { Some("loc") } else { None }
match main.0 {
"ty" => Type(name~, hole~)
"constructor" => Constructor(arguments~, ty~, throws~, hole~, source_loc~)
"getter" => {
guard result is Some(result) else {
abort("getter must have return type")
}
Getter(name~, ty~, result~, throws~, hole~, source_loc~)
}
"setter" => {
let value = arguments.get("value")
Setter(name~, ty~, value~, throws~, hole~, source_loc~)
}
"function" => {
guard result is Some(result) else {
abort("function must have return type")
}
Function(name~, ty~, arguments~, result~, throws~, hole~, source_loc~)
}
"method" => {
guard result is Some(result) else {
abort("function must have return type")
}
Method(name~, ty~, arguments~, result~, throws~, hole~, source_loc~)
}
_ => abort("\{hole}, unknown webgen attribute: \{main.0}")
}
}
///|
enum Patch {
Replace(Location, String)
Append(String)
Remove(Location)
} derive(Show, ToJson)
///|
fn longident_to_string(id : @syntax.LongIdent) -> String {
match id {
Dot(pkg~, id~) => "\{pkg}.\{id}"
Ident(name~) => name
}
}
///|
fn generate_ffi_type_annotation(ty : TypeAnnotation) -> String {
match ty {
Arrow(args, res, err~) => {
ignore((args, res, err))
abort("function type is not supported")
}
Tuple(tys) => "(" + tys.map(generate_ffi_type_annotation).join(", ") + ")"
Object(name) => "&" + longident_to_string(name)
Option(ty) => "Nullable[\{generate_ffi_type_annotation(ty)}]"
Constr(name, args) => {
let name = longident_to_string(name)
if args.is_empty() {
name
} else {
name + "[" + args.map(generate_ffi_type_annotation).join(", ") + "]"
}
}
}
}
///|
fn generate_ffi_params(arguments : Map[String, ParamDirective]) -> String {
let buf = StringBuilder::new()
let mut first = true
for _, arg in arguments {
if first {
first = false
} else {
buf.write_string(", ")
}
match arg {
Positional(name~, ty~) =>
buf.write_string("\{name} : \{generate_ffi_type_annotation(ty)}")
Optional(name~, ty~) =>
buf.write_string(
"\{name} : \{jmop_optional}[\{generate_ffi_type_annotation(ty)}]",
)
Labeled(name~, ty~) =>
buf.write_string("\{name} : \{generate_ffi_type_annotation(ty)}")
}
}
buf.to_string()
}
///|
fn generate_glude_args(arguments : Map[String, ParamDirective]) -> String {
let buf = StringBuilder::new()
let mut first = true
fn handle_nullable(arg_name : String, ty : TypeAnnotation) -> String {
match ty {
Option(_) => "\{jmop_optional}::from_option(\{arg_name})"
_ => arg_name
}
}
for _, arg in arguments {
if first {
first = false
} else {
buf.write_string(", ")
}
match arg {
Optional(name~, ty~) =>
buf
..write_string("\{jmop_optional}::from_option(")
..write_string(handle_nullable(name, ty))
..write_string(")")
Positional(name~, ty~) | Labeled(name~, ty~) =>
buf.write_string(handle_nullable(name, ty))
}
}
buf.to_string()
}
///|
fn generate_ffi_result(ty : TypeAnnotation, self_ty : String) -> String {
match ty {
Option(ty) => "\{jmop_nullable}[\{generate_ffi_type_annotation(ty)}]"
Constr(Ident(name="Self"), []) => self_ty
_ => generate_ffi_type_annotation(ty)
}
}
///|
fn generate_args_name(
arguments : Map[String, ParamDirective],
) -> (String?, String) {
let ins_name = arguments.get("self").map(d => d.name())
arguments.remove("self")
let args = StringBuilder::new()
let mut first = true
for _, arg in arguments {
if first {
first = false
} else {
args.write_string(", ")
}
args.write_string(arg.name())
}
(ins_name, args.to_string())
}
///|
fn generate_glude_error_handling(
glude : String,
throws : Array[String],
source_loc : String?,
) -> String {
if !throws.is_empty() && source_loc is Some(name) {
"\{jmop_convert_error}(\{name}, fn(){\{glude}})"
} else {
// TODO: report error if throws is not empty but source_loc is None
glude
}
}
///|
fn generate_patch(directives : List[Directive]) -> Array[Patch] {
let patches = []
for directive in directives {
match directive {
Method(name~, ty~, arguments~, result~, throws~, hole~, source_loc~) => {
let ffi_name = "ffi_\{ty}_\{name}"
let (ins_name, args_name) = generate_args_name(arguments)
guard ins_name is Some(ins_name) // TODO: handle error case
let glude_args = generate_glude_args(arguments)
let glude =
$|\{ffi_name}(self, \{glude_args})
let glude = generate_glude_error_handling(glude, throws, source_loc)
let ffi_result = generate_ffi_result(result, ty)
let ffi_params = generate_ffi_params(arguments)
let ffi =
#|#webgen.generated
$|extern "js" fn \{ffi_name}(ins : \{ty}, \{ffi_params}) -> \{ffi_result} =
$| "(\{ins_name},\{args_name}) => \{ins_name}.\{name}(\{args_name})"
patches..push(Replace(hole, glude))..push(Append(ffi))
}
Constructor(ty~, arguments~, throws~, hole~, source_loc~) => {
let ffi_name = "ffi_\{ty}_new"
let (_, args_name) = generate_args_name(arguments)
let ffi_params = generate_ffi_params(arguments)
let glude_args = generate_glude_args(arguments)
let glude =
$|\{ffi_name}(\{glude_args})
let glude = generate_glude_error_handling(glude, throws, source_loc)
let ffi =
#|#webgen.generated
$|extern "js" fn \{ffi_name}(\{ffi_params}) -> \{ty} =
$| "(\{args_name}) => new \{ty}(\{args_name})"
patches..push(Replace(hole, glude))..push(Append(ffi))
}
Getter(ty~, name~, result~, throws~, hole~, source_loc~) => {
let glude =
$|ffi_\{ty}_get_\{name}(self)
let glude = generate_glude_error_handling(glude, throws, source_loc)
let ffi_result = generate_ffi_result(result, ty)
let ffi =
#|#webgen.generated
$|extern "js" fn ffi_\{ty}_get_\{name}(ins : \{ty}) -> \{ffi_result}
$| = "(ins) => ins.\{name}"
patches..push(Replace(hole, glude))..push(Append(ffi))
}
Setter(ty~, name~, value~, throws~, hole~, source_loc~) => {
// TODO: handle error case
guard value is Some(value) else {
abort("setter must have value parameter")
}
let ffi_params = generate_ffi_params({ "value": value })
let glude =
$|ffi_\{ty}_set_\{name}(self, \{value.name()})
let glude = generate_glude_error_handling(glude, throws, source_loc)
let ffi =
#|#webgen.generated
$|extern "js" fn ffi_\{ty}_get_\{name}(ins : \{ty}, \{ffi_params}) =
$| "(ins) => ins.\{name}"
patches..push(Replace(hole, glude))..push(Append(ffi))
}
Generated(hole~) => patches.push(Remove(hole))
Type(name~, hole~) => {
let typedef =
#|#webgen.ty
$|struct \{name} (\{jmop_object})
patches.push(Replace(hole, typedef))
}
Function(..) => ()
}
}
patches
}
///|
fn snake_to_small_camel(s : StringView) -> String {
// TODO: verify input string
let parts = s.split("_").collect()
let buf = StringBuilder::new()
for i, part in parts {
if i == 0 {
buf.write_string(part.to_lower().to_string())
} else {
buf
..write_string(try! part[0:1].to_upper().to_string())
..write_string(try! part[1:].to_lower().to_string())
}
}
buf.to_string()
}
///|
test "snake_to_small_camel" {
inspect(snake_to_small_camel("hello_world"), content="helloWorld")
inspect(snake_to_small_camel("Hello_World_one"), content="helloWorldOne")
inspect(snake_to_small_camel("hello"), content="hello")
inspect(snake_to_small_camel("h"), content="h")
inspect(snake_to_small_camel(""), content="")
}
///|
fn remove_block_line(source : String) -> String {
let lines = source.split("\n").collect()
let filtered = lines.filter_map(fn(line) {
if line.has_prefix("///|") {
None
} else {
Some(line)
}
})
filtered.join("\n")
}
///|
fn apply_patch0(source : String, patches : Array[Patch]) -> String {
let appended = []
let replacements = []
for patch in patches {
match patch {
Replace(loc, s) =>
replacements.push(
multiline(
(loc.start.lnum - 1, loc.start.column() - 1),
(loc.end.lnum - 1, loc.end.column() - 2),
s,
),
)
Remove(loc) =>
replacements.push(
multiline(
(loc.start.lnum - 1, loc.start.column() - 1),
(loc.end.lnum - 1, loc.end.column() - 2),
"",
),
)
Append(text) => appended.push(text)
}
}
let buf = StringBuilder::new(size_hint=source.length() * 2)
buf.write_string(splice(source, replacements))
for text in appended {
buf.write_string("\n")
buf.write_string(text)
}
buf.to_string()
}