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:
		
							parent
							
								
									69cc34d07e
								
							
						
					
					
						commit
						ec7ee9626f
					
				
					 4 changed files with 125 additions and 50 deletions
				
			
		| 
						 | 
				
			
			@ -361,52 +361,43 @@ 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 == "=" {
 | 
			
		||||
        c.emit(Diagnostic::error(
 | 
			
		||||
            src.ast.span(op),
 | 
			
		||||
            "defs `a = b` may only appear at the top level",
 | 
			
		||||
        ));
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    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",
 | 
			
		||||
            ));
 | 
			
		||||
            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) {
 | 
			
		||||
                let argument_count = 2;
 | 
			
		||||
                c.chunk.emit_opcode(Opcode::System)?;
 | 
			
		||||
                c.chunk.emit_u8(index)?;
 | 
			
		||||
                c.chunk.emit_u8(argument_count)?;
 | 
			
		||||
            } else {
 | 
			
		||||
                c.emit(Diagnostic::error(
 | 
			
		||||
                    src.ast.span(op),
 | 
			
		||||
                    "this binary operator is currently unimplemented",
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    compile_expr(c, src, left)?;
 | 
			
		||||
    compile_expr(c, src, right)?;
 | 
			
		||||
    if let Some(index) = system::resolve(SystemFnArity::Binary, name) {
 | 
			
		||||
        let argument_count = 2;
 | 
			
		||||
        c.chunk.emit_opcode(Opcode::System)?;
 | 
			
		||||
        c.chunk.emit_u8(index)?;
 | 
			
		||||
        c.chunk.emit_u8(argument_count)?;
 | 
			
		||||
    } else {
 | 
			
		||||
        c.emit(Diagnostic::error(
 | 
			
		||||
            src.ast.span(op),
 | 
			
		||||
            "this unary 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(());
 | 
			
		||||
    };
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,8 @@ pub enum TokenKind {
 | 
			
		|||
    Greater,
 | 
			
		||||
    GreaterEqual,
 | 
			
		||||
    Not,
 | 
			
		||||
    Dot,
 | 
			
		||||
    Pipe,
 | 
			
		||||
 | 
			
		||||
    // Punctuation
 | 
			
		||||
    Newline,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue