import eyg/ir/dag_json import eyg/ir/tree as ir import eyg/parser/token as t import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/result.{try} import gleam/string import multiformats/cid/v1 pub type Reason { UnexpectEnd UnexpectedToken(token: t.Token, position: Int) // Raised when `=` is missing in a let binding: `let x 5` MissingEquals(position: Int) // Raised when `->` or the opening `{` is missing in a function: `(x) { x }` MissingArrow(position: Int) // Raised when the closing `}` of a function body is not found UnclosedFunctionBody(open_at: Int) // Raised when perform/handle is not followed by an uppercase effect name ExpectedEffectName(keyword: String, position: Int) // Raised when `!` is not followed by a lowercase builtin identifier ExpectedBuiltinName(position: Int) // Raised when `#` is followed by a label that is not a valid CID InvalidCidReference(position: Int) // Raised when `import` is not followed by a string path literal InvalidImportPath(position: Int) // Raised when tokens remain after a complete expression is parsed TrailingTokens(token: t.Token, position: Int) // Raised for characters the lexer does not recognise (e.g. `+`, `` ` ``) InvalidCharacter(char: String, position: Int) // Raised when a string literal is not closed before end of input UnterminatedStringLiteral(position: Int) // Raised when a string contains an unrecognised escape sequence (e.g. `\q`) InvalidEscapeSequence(escape_char: String, position: Int) } pub type Span = #(Int, Int) pub type Match = #(#(String, Span), Option(#(Span, #(String, Span)))) pub type Pattern { Assign(String) Destructure(List(Match)) } fn do_destructure(tokens, acc) { case tokens { [#(t.RightBrace, _), ..rest] -> Ok(#(acc, rest)) // can be spaces between colon and rest [#(t.Name(field), f), #(t.Colon, c), #(t.Name(var), v), ..rest] -> { let field = #(field, #(f, f + string.length(field))) let colon = #(c, c + 1) let var = #(var, #(v, v + string.length(var))) let acc = [#(field, Some(#(colon, var))), ..acc] case rest { [#(t.RightBrace, _), ..rest] -> Ok(#(acc, rest)) [#(t.Comma, _), ..rest] -> do_destructure(rest, acc) _ -> fail(rest) } } [#(t.Name(field), start), ..rest] -> { let field = #(field, #(start, start + string.length(field))) let acc = [#(field, None), ..acc] case rest { [#(t.RightBrace, _), ..rest] -> Ok(#(acc, rest)) [#(t.Comma, _), ..rest] -> do_destructure(rest, acc) _ -> fail(rest) } } _ -> fail(tokens) } } fn one_pattern(tokens) { case tokens { [#(t.Name(label), _), ..rest] -> Ok(#(Assign(label), rest)) [#(t.LeftBrace, _), ..rest] -> { use #(matches, rest) <- try(do_destructure(rest, [])) Ok(#(Destructure(matches), rest)) } _ -> fail(tokens) } } pub fn do_patterns(tokens, acc) { use #(pattern, tokens) <- try(one_pattern(tokens)) let acc = [pattern, ..acc] use #(#(next, start), rest) <- try(pop(tokens)) case next { t.Comma -> do_patterns(rest, acc) t.RightParen -> Ok(#(acc, rest)) _ -> Error(UnexpectedToken(next, start)) } } pub fn destructured(matches: List(Match), term: #(ir.Expression(_), Span)) { list.fold(matches, term, fn(acc, pair) { let #(field, assign) = pair let #(field, fspan) = field let #(cspan, #(var, _vspan)) = case assign { Some(#(cspan, #(var, vspan))) -> #(cspan, #(var, vspan)) None -> #(fspan, #(field, fspan)) } let aspan = #(fspan.0, cspan.1) let lspan = #(fspan.0, term.1.1) #( ir.Let( var, #( ir.Apply(#(ir.Select(field), fspan), #(ir.Variable("$"), cspan)), aspan, ), acc, ), lspan, ) }) } pub fn block(tokens) { use #(#(token, start), rest) <- try(pop(tokens)) case token { t.Let -> { use #(pattern, rest) <- try(one_pattern(rest)) use rest <- try(case rest { [#(t.Equal, _), ..rest] -> Ok(rest) [#(_, at), ..] -> Error(MissingEquals(at)) [] -> Error(UnexpectEnd) }) use #(value, rest) <- try(expression(rest)) case block(rest) { Ok(#(then, rest)) -> { let #(_, #(_start, end)) = then let span = #(start, end) let exp = case pattern { Assign(label) -> #(ir.Let(label, value, then), span) Destructure(matches) -> #( ir.Let("$", value, destructured(matches, then)), span, ) } Ok(#(exp, rest)) } Error(UnexpectEnd) -> { let span = #(start, start) let then = #(ir.Vacant, #(0, 0)) let exp = case pattern { Assign(label) -> #(ir.Let(label, value, then), span) Destructure(matches) -> #( ir.Let("$", value, destructured(matches, then)), span, ) } Ok(#(exp, rest)) } Error(other) -> Error(other) } } _ -> { expression(tokens) } } } pub fn expression(tokens) { use #(#(token, start), rest) <- try(pop(tokens)) use #(exp, rest) <- try(case token { t.Name(label) -> { let span = #(start, start + string.length(label)) Ok(#(#(ir.Variable(label), span), rest)) } t.Let -> { use #(pattern, rest) <- try(one_pattern(rest)) use rest <- try(case rest { [#(t.Equal, _), ..rest] -> Ok(rest) [#(_, at), ..] -> Error(MissingEquals(at)) [] -> Error(UnexpectEnd) }) use #(value, rest) <- try(expression(rest)) use #(then, rest) <- try(expression(rest)) let #(_, #(_start, end)) = then let span = #(start, end) let exp = case pattern { Assign(label) -> #(ir.Let(label, value, then), span) Destructure(matches) -> #( ir.Let("$", value, destructured(matches, then)), span, ) } Ok(#(exp, rest)) } t.LeftParen -> { use #(patterns_reversed, rest) <- try(do_patterns(rest, [])) use #(rest, brace_at) <- try(case rest { [#(t.RightArrow, _), #(t.LeftBrace, brace_at), ..rest] -> Ok(#(rest, brace_at)) // `->` present but `{` missing — point to the token where `{` was expected [#(t.RightArrow, _), #(_, at), ..] -> Error(MissingArrow(at)) [#(t.RightArrow, arrow_at)] -> Error(MissingArrow(arrow_at + 2)) [#(_, at), ..] -> Error(MissingArrow(at)) [] -> Error(UnexpectEnd) }) use #(body, rest) <- try(expression(rest)) use #(rest, end) <- try(case rest { [#(t.RightBrace, end), ..rest] -> Ok(#(rest, end)) _ -> Error(UnclosedFunctionBody(brace_at)) }) let span = #(start, end + 1) let exp = list.fold(patterns_reversed, body, fn(body, pattern) { case pattern { Assign(label) -> #(ir.Lambda(label, body), span) Destructure(matches) -> #( ir.Lambda("$", destructured(matches, body)), span, ) } }) Ok(#(exp, rest)) } t.Integer(raw) -> { let assert Ok(value) = int.parse(raw) let span = #(start, start + string.length(raw)) Ok(#(#(ir.Integer(value), span), rest)) } t.Minus -> { use #(#(next, from), rest) <- try(pop(rest)) case next { t.Integer(raw) -> { let assert Ok(value) = int.parse(raw) let span = #(start, from + string.length(raw)) Ok(#(#(ir.Integer(-1 * value), span), rest)) } _ -> Error(UnexpectedToken(token, start)) } } t.String(value) -> { let span = #(start, start + string.length(value) + 2) Ok(#(#(ir.String(value), span), rest)) } t.LeftSquare -> do_list(rest, start, []) t.LeftBrace -> do_record(rest, start, []) t.Uppername(label) -> { let span = #(start, start + string.length(label)) Ok(#(#(ir.Tag(label), span), rest)) } t.Match -> { case rest { [#(t.LeftBrace, _), ..rest] -> { use #(exp, _, rest) <- try(clauses(rest, start)) Ok(#(exp, rest)) } _ -> { use #(subject, rest) <- try(expression(rest)) case rest { [#(t.LeftBrace, inner), ..rest] -> { use #(exp, end, rest) <- try(clauses(rest, inner)) let span = #(start, end) Ok(#(#(ir.Apply(exp, subject), span), rest)) } _ -> fail(rest) } } } } t.Perform -> case rest { [#(t.Uppername(label), end), ..rest] -> { let span = #(start, end + string.length(label)) Ok(#(#(ir.Perform(label), span), rest)) } _ -> Error(ExpectedEffectName("perform", next_pos(rest, start + 7))) } t.Handle -> case rest { [#(t.Uppername(label), end), ..rest] -> { let span = #(start, end + string.length(label)) Ok(#(#(ir.Handle(label), span), rest)) } _ -> Error(ExpectedEffectName("handle", next_pos(rest, start + 6))) } t.Bang -> case rest { [#(t.Name(label), end), ..rest] -> { let span = #(start, end + string.length(label)) Ok(#(#(ir.Builtin(label), span), rest)) } _ -> Error(ExpectedBuiltinName(next_pos(rest, start + 1))) } t.Hash -> case rest { [#(t.Name(label), end), ..rest] -> { let span = #(start, end + string.length(label)) case v1.from_string(label) { Ok(#(cid, _)) -> Ok(#(#(ir.ContentReference(cid), span), rest)) Error(_) -> Error(InvalidCidReference(end)) } } _ -> Error(InvalidCidReference(next_pos(rest, start + 1))) } t.At -> case rest { [#(t.Name(label), end), ..rest] -> { let span = #(start, end + string.length(label)) Ok(#( #(ir.ReleaseReference(label, 0, dag_json.vacant_cid), span), rest, )) } _ -> fail(rest) } t.Import -> case rest { [#(t.String(value), end), ..rest] -> { let span = #(start, end + string.length(value) + 2) Ok(#(#(ir.RelativeReference(value), span), rest)) } _ -> Error(InvalidImportPath(next_pos(rest, start + 6))) } t.UnexpectedGrapheme(raw) -> Error(InvalidCharacter(string.slice(raw, 0, 1), start)) t.UnterminatedString(_) -> Error(UnterminatedStringLiteral(start)) t.InvalidEscape(raw) -> Error(InvalidEscapeSequence(string.slice(raw, 1, 1), start)) _ -> Error(UnexpectedToken(token, start)) }) after_expression(exp, rest) } fn after_expression(exp, rest) { case rest { [#(t.LeftParen, _start), ..rest] -> { use #(arg, rest) <- try(expression(rest)) use #(args, _end, rest) <- try(do_args(rest, [arg])) let args = list.reverse(args) let exp = list.fold(args, exp, fn(acc, arg) { let #(_, #(start, _)) = acc let #(_, #(_, end)) = arg #(ir.Apply(acc, arg), #(start, end + 1)) }) after_expression(exp, rest) } [#(t.Dot, dot_at), #(t.Name(label), name_at), ..rest] -> { let end = name_at + string.length(label) let select = #(ir.Select(label), #(dot_at, end)) let #(_value, #(start, _)) = exp let span = #(start, end) after_expression(#(ir.Apply(select, exp), span), rest) } _ -> Ok(#(exp, rest)) } } fn do_args(tokens, acc) { case tokens { [#(t.RightParen, end), ..rest] -> Ok(#(acc, end + 1, rest)) [#(t.Comma, _), ..rest] -> { use #(arg, rest) <- try(expression(rest)) do_args(rest, [arg, ..acc]) } _ -> fail(tokens) } } fn fail(tokens) { case tokens { [] -> Error(UnexpectEnd) [#(t, start), ..] -> Error(UnexpectedToken(t, start)) } } fn next_pos(rest, fallback) { case rest { [#(_, at), ..] -> at [] -> fallback } } // this supports trailing comma fn do_list(tokens, start, acc) { // use #(t,rest) case tokens { [] -> Error(UnexpectEnd) [#(t.RightSquare, end), ..rest] -> { let span = #(start, end + 1) Ok(#(build_list(acc, #(ir.Tail, span)), rest)) } _ -> { use #(item, rest) <- try(expression(tokens)) let acc = [#(start, item), ..acc] case rest { [#(t.Comma, _), #(t.DotDot, _), ..rest] -> { use #(tail, rest) <- try(expression(rest)) use #(#(token, start), rest) <- try(pop(rest)) case token { t.RightSquare -> Ok(#(build_list(acc, tail), rest)) _ -> Error(UnexpectedToken(token, start)) } } [#(t.Comma, start), ..rest] -> do_list(rest, start, acc) [#(t.RightSquare, start), ..rest] -> { let span = #(start, start + 1) Ok(#(build_list(acc, #(ir.Tail, span)), rest)) } [#(t, start), ..] -> Error(UnexpectedToken(t, start)) [] -> Error(UnexpectEnd) } } } } pub fn build_list(reversed, acc) { case reversed { [#(from, item), ..rest] -> { let #(_, #(_, c)) = acc let #(_, #(_, b)) = item build_list( rest, #( ir.Apply( #(ir.Apply(#(ir.Cons, #(from, from + 1)), item), #(from, b)), acc, ), #(from, c), ), ) } [] -> acc } } fn do_record(rest, start, acc) { use #(#(token, kstart), rest) <- try(pop(rest)) case token { t.RightBrace -> Ok(#(#(ir.Empty, #(start, kstart + 1)), rest)) t.Name(label) -> { use #(#(token, next), rest) <- try(pop(rest)) case token { t.Colon -> { use #(value, rest) <- try(expression(rest)) let acc = [#(#(start, next + 1), label, value), ..acc] // replace above with field function case rest { [#(t.Comma, start), ..rest] -> do_record(rest, start, acc) [#(t.RightBrace, start), ..rest] -> { let span = #(start, start + 1) Ok(#(build_record(acc, #(ir.Empty, span)), rest)) } _ -> fail(rest) } } t.Comma -> { let acc = [ #( // kstart is the label starting position #(start, kstart + string.length(label)), label, #(ir.Variable(label), #(kstart, kstart + string.length(label))), ), ..acc ] do_record(rest, next, acc) } t.RightBrace -> { let acc = [ #( #(start, kstart + string.length(label)), label, #(ir.Variable(label), #(kstart, kstart + string.length(label))), ), ..acc ] let span = #(next, next + 1) Ok(#(build_record(acc, #(ir.Empty, span)), rest)) } _ -> Error(UnexpectedToken(token, start)) } } t.DotDot -> { use #(value, rest) <- try(expression(rest)) use #(#(token, start), rest) <- try(pop(rest)) use rest <- try(case token { // could include right brace in the tail for editing t.RightBrace -> Ok(rest) _ -> Error(UnexpectedToken(token, start)) }) Ok(#(build_overwrite(acc, value), rest)) } _ -> Error(UnexpectedToken(token, start)) } } pub fn build_record(reversed, acc) { case reversed { [#(span, label, item), ..rest] -> { let #(_, #(_, c)) = acc let #(_, #(_, b)) = item let #(a, _) = span build_record( rest, #( ir.Apply(#(ir.Apply(#(ir.Extend(label), span), item), #(a, b)), acc), #(a, c), ), ) } [] -> acc } } pub fn build_overwrite(reversed, acc) { case reversed { [#(span, label, item), ..rest] -> { let #(_, #(_, c)) = acc let #(_, #(_, b)) = item let #(a, _) = span build_overwrite( rest, #( ir.Apply( #(ir.Apply(#(ir.Overwrite(label), span), item), #(a, b)), acc, ), #(a, c), ), ) } [] -> acc } } fn clauses(tokens, start) { use #(clauses, tail, rest) <- try(do_clauses(tokens, start, [])) let #(_, #(_, end)) = tail let exp = list.fold(clauses, tail, fn(exp, clause) { let #(start, label, cspan, branch) = clause let case_ = #(ir.Case(label), cspan) let #(_, #(_, branch_end)) = branch let inner = #(ir.Apply(case_, branch), #(cspan.0, branch_end)) let #(_, #(_, final)) = tail #(ir.Apply(inner, exp), #(start, final)) }) Ok(#(exp, end, rest)) } fn do_clauses(tokens, start, acc) { use #(#(token, clause), rest) <- try(pop(tokens)) case token { t.RightBrace -> Ok(#(acc, #(ir.NoCases, #(start, clause + 1)), rest)) t.Uppername(label) -> { use #(branch, rest) <- try(expression(rest)) let acc = [ #(start, label, #(clause, clause + string.length(label)), branch), ..acc ] case rest { [#(_, last), ..] -> do_clauses(rest, last, acc) [] -> Error(UnexpectEnd) } } // Open function is parens that are treated as a call to the line above // uppername can never be a tag expression because return from Tag is not another fn t.Bar -> { use #(#(otherwise, span), rest) <- try(expression(rest)) case rest { [#(t.RightBrace, _), ..rest] -> Ok(#(acc, #(otherwise, span), rest)) _ -> fail(rest) } } _ -> Error(UnexpectedToken(token, clause)) } } fn pop(tokens) { case tokens { [t, ..rest] -> Ok(#(t, rest)) [] -> Error(UnexpectEnd) } } pub fn describe_reason(reason) { case reason { UnexpectedToken(token:, position:) -> "unexpected `" <> t.to_string(token) <> "` at position " <> int.to_string(position) UnexpectEnd -> "unexpected end of input" InvalidCharacter(char:, position:) -> "invalid character '" <> char <> "' at position " <> int.to_string(position) <> "\nhint: remove or replace this character — EYG does not use it" UnterminatedStringLiteral(position:) -> "unterminated string literal at position " <> int.to_string(position) <> "\nhint: close the string with a double-quote `\"`" InvalidEscapeSequence(escape_char:, position:) -> "invalid escape sequence `\\" <> escape_char <> "` in string at position " <> int.to_string(position) <> "\nhint: valid escapes are \\n (newline), \\t (tab), \\r (carriage return), \\\" (quote), \\\\ (backslash)" MissingEquals(position:) -> "expected `=` after let binding name at position " <> int.to_string(position) <> "\nhint: let bindings use the form `let name = expression`" MissingArrow(position:) -> "expected `->` followed by `{` in function definition at position " <> int.to_string(position) <> "\nhint: functions are written as `(arg) -> { body }`" UnclosedFunctionBody(open_at:) -> "unclosed function body — expected `}` to close the `{` opened at position " <> int.to_string(open_at) <> "\nhint: every `{` in a function body must be closed with `}`" ExpectedEffectName(keyword:, position:) -> "expected an uppercase effect name after `" <> keyword <> "` at position " <> int.to_string(position) <> "\nhint: effect names must start with an uppercase letter, e.g. `" <> keyword <> " Log`" ExpectedBuiltinName(position:) -> "expected a builtin identifier after `!` at position " <> int.to_string(position) <> "\nhint: builtins use lowercase names, e.g. `!int_add`" InvalidCidReference(position:) -> "invalid content identifier (CID) after `#` at position " <> int.to_string(position) <> "\nhint: CID references use a valid base32-encoded CID, e.g. `#bafyreig...`" InvalidImportPath(position:) -> "expected a string path after `import` at position " <> int.to_string(position) <> "\nhint: import paths must be string literals, e.g. `import \"./module.eyg.json\"`" TrailingTokens(token:, position:) -> { let token_str = case token { t.UnexpectedGrapheme(raw) -> string.slice(raw, 0, 1) _ -> t.to_string(token) } let hint = case token { t.Let -> "hint: the previous expression already completed the block. If you meant the block to continue, bind that expression to `let _ = ...` first." _ -> "hint: EYG uses function calls for operations, not infix operators" } "unexpected `" <> token_str <> "` at position " <> int.to_string(position) <> " — the expression is complete but there are leftover tokens\n" <> hint } } }