implement . and | operators

`f . x` is the same as `f x`, and is mostly useful when used as an argument to another function call.
this allows us to simulate record field syntax very well: `d.Num` is the same as `d Num`, but with high precedence

`a |f b` is the same as `f a b` and effectively allows you to turn any function into an arithmetic operator.
it's the main reason behind + - * / having the same precedence---they now chain very nicely with pipes

closes #126 (`|` operator)
closes #127 (`.` operator)
This commit is contained in:
りき萌 2025-09-02 22:03:42 +02:00
parent 69cc34d07e
commit ec7ee9626f
4 changed files with 125 additions and 50 deletions

View file

@ -361,14 +361,18 @@ fn compile_binary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -
}
let name = src.ast.span(op).slice(src.code);
if name == "=" {
match name {
":" => {
// Invalid use of a def inside an expression.
c.emit(Diagnostic::error(
src.ast.span(op),
"defs `a = b` may only appear at the top level",
"defs `a: b` may only appear at the top level",
));
return Ok(());
Ok(())
}
"." => compile_dot_call(c, src, left, right),
"|" => compile_pipe_call(c, src, left, right),
_ => {
compile_expr(c, src, left)?;
compile_expr(c, src, right)?;
if let Some(index) = system::resolve(SystemFnArity::Binary, name) {
@ -379,34 +383,21 @@ fn compile_binary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -
} else {
c.emit(Diagnostic::error(
src.ast.span(op),
"this unary operator is currently unimplemented",
"this binary operator is currently unimplemented",
));
}
Ok(())
}
fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
let mut walk = src.ast.walk(node_id);
let Some(func) = walk.node() else {
return Ok(());
};
let name = src.ast.span(func).slice(src.code);
let mut argument_count = 0;
while let Some(arg) = walk.node() {
compile_expr(c, src, arg)?;
argument_count += 1;
}
}
let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| {
c.emit(Diagnostic::error(
src.ast.span(node_id),
"function call has too many arguments",
));
0
});
fn emit_nary_call<'a>(
c: &mut Compiler<'a>,
src: &Source<'a>,
func: NodeId,
argument_count: u8,
) -> CompileResult {
let name = src.ast.span(func).slice(src.code);
if let (NodeKind::Ident, Some(index)) = (
src.ast.kind(func),
system::resolve(SystemFnArity::Nary, name),
@ -423,6 +414,81 @@ fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) ->
c.chunk.emit_opcode(Opcode::Call)?;
c.chunk.emit_u8(argument_count)?;
}
Ok(())
}
fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
let mut walk = src.ast.walk(node_id);
let Some(func) = walk.node() else {
return Ok(());
};
let mut argument_count = 0;
while let Some(arg) = walk.node() {
compile_expr(c, src, arg)?;
argument_count += 1;
}
let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| {
c.emit(Diagnostic::error(
src.ast.span(node_id),
"function call has too many arguments",
));
0
});
emit_nary_call(c, src, func, argument_count)?;
Ok(())
}
fn compile_dot_call<'a>(
c: &mut Compiler<'a>,
src: &Source<'a>,
func: NodeId,
right: NodeId,
) -> CompileResult {
compile_expr(c, src, right)?;
emit_nary_call(c, src, func, 1)?;
Ok(())
}
fn compile_pipe_call<'a>(
c: &mut Compiler<'a>,
src: &Source<'a>,
left: NodeId,
call: NodeId,
) -> CompileResult {
if src.ast.kind(call) != NodeKind::Call {
c.emit(Diagnostic::error(
src.ast.span(call),
"the right side of a pipe `|` must be a function call",
));
return Ok(());
}
let mut walk = src.ast.walk(call);
let Some(func) = walk.node() else {
return Ok(());
};
compile_expr(c, src, left)?;
let mut argument_count = 1;
while let Some(arg) = walk.node() {
compile_expr(c, src, arg)?;
argument_count += 1;
}
let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| {
c.emit(Diagnostic::error(
src.ast.span(call),
"function call has too many arguments",
));
0
});
emit_nary_call(c, src, func, argument_count)?;
Ok(())
}

View file

@ -215,6 +215,8 @@ fn token(l: &mut Lexer<'_>) -> (TokenKind, Span, bool) {
'!' => one_or_two(l, TokenKind::Not, '=', TokenKind::NotEqual),
'<' => one_or_two(l, TokenKind::Less, '=', TokenKind::LessEqual),
'>' => one_or_two(l, TokenKind::Greater, '=', TokenKind::GreaterEqual),
'.' => one(l, TokenKind::Dot),
'|' => one(l, TokenKind::Pipe),
'\n' => return newline(l, has_left_space),
'(' => one(l, TokenKind::LParen),

View file

@ -327,17 +327,20 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter {
| TokenKind::LessEqual
| TokenKind::Greater
| TokenKind::GreaterEqual => 2,
TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::Slash => 3,
// 4: reserve for `.`
TokenKind::Plus
| TokenKind::Minus
| TokenKind::Star
| TokenKind::Slash
| TokenKind::Pipe => 3,
TokenKind::Dot => 4,
_ if is_prefix_token((kind, spaces)) => 5,
_ => return None, // not an infix operator
};
Some(match kind {
// There are a few types of operators which are independent of tightness.
// For : and =, it does not matter if they're spelled one way or the other, because
// there is only one way to use them (at the beginning of the expression).
TokenKind::Colon | TokenKind::Equal => Tightness::Loose(index),
// There are a few types of operators which work independent of tightness.
TokenKind::Colon | TokenKind::Equal | TokenKind::Pipe => Tightness::Loose(index),
// For unary -, we treat it as having Tight spacing rather than Call, else it would
// be allowed to begin function calls.
@ -652,7 +655,9 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind {
| TokenKind::LessEqual
| TokenKind::Greater
| TokenKind::GreaterEqual
| TokenKind::Colon => infix_binary(p, op),
| TokenKind::Colon
| TokenKind::Dot
| TokenKind::Pipe => infix_binary(p, op),
TokenKind::Minus if spaces.are_balanced() => infix_binary(p, op),
TokenKind::Equal => infix_let(p, op),

View file

@ -28,6 +28,8 @@ pub enum TokenKind {
Greater,
GreaterEqual,
Not,
Dot,
Pipe,
// Punctuation
Newline,