sabre

package module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 1, 2020 License: GPL-3.0 Imports: 14 Imported by: 4

README

Sabre

GoDoc Go Report Card Build Status

Sabre is highly customizable, embeddable LISP engine for Go.

Check out Slang for a tiny LISP written using Sabre.

Features

  • Highly Customizable reader/parser through a read table (Inspired by Clojure) (See Reader)
  • Built-in data types: nil, bool, string, number, character, keyword, symbol, list, vector, set, module
  • Multiple number formats supported: decimal, octal, hexadecimal, radix and scientific notations.
  • Full unicode support. Symbols can include unicode characters (Example: find-δ, π etc.) and 🧠, 🏃 etc. (yes, smileys too).
  • Character Literals with support for:
    1. simple literals (e.g., \a for a)
    2. special literals (e.g., \newline, \tab etc.)
    3. unicode literals (e.g., \u00A5 for ¥ etc.)
  • Clojure style built-in special forms: fn*, def, if, do, throw, let*
  • Simple interface sabre.Value and optional sabre.Invokable, sabre.Seq interfaces for adding custom data types. (See Evaluation)
  • A macro system.

Please note that Sabre is NOT an implementation of a particular LISP dialect. It provides pieces that can be used to build a LISP dialect or can be used as a scripting layer.

Usage

What can you use it for?

  1. Embedded script engine to provide dynamic behavior without requiring re-compilation of your application.
  2. Business rule engine by exposing very specific & composable rule functions.
  3. To build your own LISP dialect.

Sabre requires Go 1.13 or higher.

As Embedded Script Engine

Sabre has concept of Scope which is responsible for maintaining bindings. You can bind any Go value and access it using LISP code, which makes it possible to expose parts of your API and make it scriptable or build your own LISP dialect. Also, See Extending for more information on customizing the reader or eval.

package main

import "github.com/spy16/sabre"

func main() {
    scope := sabre.NewScope(nil)
    _ = scope.BindGo("inc", func(v int) int { return v+1 })

    result, _ := sabre.ReadEvalStr(scope, "(inc 10)")
    fmt.Printf("Result: %v\n", result) // should print "Result: 11"
}
Expose through a REPL

Sabre comes with a tiny repl package that is very flexible and easy to setup to expose your LISP through a read-eval-print-loop.

package main

import (
  "log"

  "github.com/spy16/sabre"
  "github.com/spy16/sabre/repl"
)

func main() {
  scope := sabre.NewScope(nil)
  scope.BindGo("inc", func(v int) int { return v+1 })

  repl.New(scope,
    repl.WithBanner("Welcome to my own LISP!"),
    repl.WithPrompts("=>", "|"),
    // many more options available
  ).Loop(context.Background())
}
Standalone

Sabre has a small reference LISP dialect named Slang (short for Sabre Lang) for which a standalone binary is available. Check out Slang for instructions on installing Slang.

Extending

Reader

Sabre reader is inspired by Clojure reader and uses a read table. Reader supports following forms:

  • Numbers:
    • Integers use int64 Go representation and can be specified using decimal, binary hexadecimal or radix notations. (e.g., 123, -123, 0b101011, 0xAF, 2r10100, 8r126 etc.)
    • Floating point numbers use float64 Go representation and can be specified using decimal notation or scientific notation. (e.g.: 3.1412, -1.234, 1e-5, 2e3, 1.5e3 etc.)
  • Characters: Characters use rune or uint8 Go representation and can be written in 3 ways:
    • Simple: \a, , etc.
    • Special: \newline, \tab etc.
    • Unicode: \u1267
  • Boolean: true or false are converted to Bool type.
  • Nil: nil is represented as a zero-allocation empty struct in Go.
  • Keywords: Keywords are like symbols but start with : and evaluate to themselves.
  • Symbols: Symbols can be used to name a value and can contain any Unicode symbol.
  • Lists: Lists are zero or more forms contained within parenthesis. (e.g., (1 2 3), (1 [])). Evaluating a list leads to an invocation.
  • Vectors: Vectors are zero or more forms contained within brackets. (e.g., [], [1 2 3])
  • Sets: Set is a container for zero or more unique forms. (e.g. #{1 2 3})

Reader can be extended to add new syntactical features by adding reader macros to the read table. Reader Macros are implementations of sabre.ReaderMacro function type. Except numbers and symbols, everything else supported by the reader is implemented using reader macros.

Evaluation
  • Keyword, String, Int, Float, Character, Bool, nil, MultiFn, Fn, Type and Any evaluate to themselves.
  • Symbol is resolved as follows:
    • If symbol has no ., symbol is directly used to lookup in current Scope to find the value.
    • If symbol is qualified (i.e., contains .), symbol is split using . as delimiter and first field is resolved as per previous rule and rest of the fields are recursively resolved as members. (For example, foo.Bar.Baz: foo is resolved from scope, Bar should be member of value of foo. And Baz should be member of value resolved for foo.Bar)
  • Evaluating Vector & Set simply yields new vector and set whose values are evaluated values contained in the original vector and set.
  • Evaluating Module evaluates all the forms in the module and returns the result of last evaluation. Any error stops the evaluation process.
  • Empty List is returned as is.
  • Non empty List is an invocation and evaluated using following rules:
    • If the first argument resolves to a special-form (SpecialForm Go type), it is invoked and return value is cached in the list. This return value is used for evaluating the list.
    • If the first argument resolves to a Macro, macro is invoked with the rest of the list as arguments and return value replaces the list with (do retval) form.
    • If first value resolves to an Invokable value, Invoke() is called. Functions are implemented using MultiFn which implements Invokable. Vector also implements Invokable and provides index access.
    • It is an error.

Documentation

Overview

Package sabre provides data structures, reader for reading LISP source into data structures and functions for evluating forms against a context.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSkip can be returned by reader macro to indicate a no-op form which
	// should be discarded (e.g., Comments).
	ErrSkip = errors.New("skip expr")

	// ErrEOF is returned when stream ends prematurely to indicate that more
	// data is needed to complete the current form.
	ErrEOF = errors.New("unexpected EOF")
)
View Source
var (
	// Def implements (def symbol value) form for defining bindings.
	Def = SpecialForm{
		Name:  "def",
		Parse: parseDef,
	}

	// Lambda defines an anonymous function and returns. Must have the form
	// (fn* name? [arg*] expr*) or (fn* name? ([arg]* expr*)+)
	Lambda = SpecialForm{
		Name:  "fn*",
		Parse: fnParser(false),
	}

	// Macro defines an anonymous function and returns. Must have the form
	// (macro* name? [arg*] expr*) or (fn* name? ([arg]* expr*)+)
	Macro = SpecialForm{
		Name:  "macro*",
		Parse: fnParser(true),
	}

	// Let implements the (let [binding*] expr*) form. expr are evaluated
	// with given local bindings.
	Let = SpecialForm{
		Name:  "let",
		Parse: parseLet,
	}

	// Do special form evaluates args one by one and returns the result of
	// the last expr.
	Do = SpecialForm{
		Name:  "do",
		Parse: parseDo,
	}

	// If implements if-conditional flow using (if test then else?) form.
	If = SpecialForm{
		Name:  "if",
		Parse: parseIf,
	}

	// SimpleQuote prevents a form from being evaluated.
	SimpleQuote = SpecialForm{
		Name:  "quote",
		Parse: parseSimpleQuote,
	}

	// SyntaxQuote recursively applies the quoting to the form.
	SyntaxQuote = SpecialForm{
		Name:  "syntax-quote",
		Parse: parseSyntaxQuote,
	}
)
View Source
var ErrResolving = errors.New("unable to resolve symbol")

ErrResolving is returned when a scope implementation fails to resolve a binding for given symbol.

Functions

func Compare added in v0.2.1

func Compare(v1, v2 Value) bool

Compare compares two values in an identity independent manner. If v1 has `Compare(Value) bool` method, the comparison is delegated to it as `v1.Compare(v2)`.

Types

type Any added in v0.2.3

type Any struct{ V reflect.Value }

Any can be used to wrap arbitrary Go value into Sabre scope.

func (Any) Eval added in v0.2.3

func (any Any) Eval(_ Scope) (Value, error)

Eval returns itself.

func (Any) String added in v0.2.3

func (any Any) String() string

type Bool added in v0.1.2

type Bool bool

Bool represents a boolean value.

func (Bool) Eval added in v0.1.2

func (b Bool) Eval(_ Scope) (Value, error)

Eval returns the underlying value.

func (Bool) String added in v0.1.2

func (b Bool) String() string

type Character added in v0.1.2

type Character rune

Character represents a character literal. For example, \a, \b, \1, \∂ etc are valid character literals. In addition, special literals like \newline, \space etc are supported by the reader.

func (Character) Eval added in v0.1.2

func (char Character) Eval(_ Scope) (Value, error)

Eval simply returns itself since Chracters evaluate to themselves.

func (Character) String added in v0.1.2

func (char Character) String() string

type EvalError added in v0.2.0

type EvalError struct {
	Position
	Cause error
	Form  Value
}

EvalError represents error during evaluation.

func (EvalError) Error added in v0.2.0

func (ee EvalError) Error() string

func (EvalError) Unwrap added in v0.2.0

func (ee EvalError) Unwrap() error

Unwrap returns the underlying cause of this error.

type Float64 added in v0.1.2

type Float64 float64

Float64 represents double precision floating point numbers represented using decimal or scientific number formats.

func (Float64) Eval added in v0.1.2

func (f64 Float64) Eval(_ Scope) (Value, error)

Eval simply returns itself since Floats evaluate to themselves.

func (Float64) String added in v0.1.2

func (f64 Float64) String() string

type Fn added in v0.1.3

type Fn struct {
	Args     []string
	Variadic bool
	Body     Value
	Func     func(scope Scope, args []Value) (Value, error)
}

Fn represents a function or macro definition.

func (*Fn) Compare added in v0.3.1

func (fn *Fn) Compare(v Value) bool

Compare returns true if 'other' is also a function and has the same signature and body.

func (*Fn) Eval added in v0.2.2

func (fn *Fn) Eval(_ Scope) (Value, error)

Eval returns the function itself.

func (*Fn) Invoke added in v0.1.3

func (fn *Fn) Invoke(scope Scope, args ...Value) (Value, error)

Invoke executes the function with given arguments.

func (Fn) String added in v0.2.2

func (fn Fn) String() string

type Int64 added in v0.1.2

type Int64 int64

Int64 represents integer values represented using decimal, octal, radix and hexadecimal formats.

func (Int64) Eval added in v0.1.2

func (i64 Int64) Eval(_ Scope) (Value, error)

Eval simply returns itself since Integers evaluate to themselves.

func (Int64) String added in v0.1.2

func (i64 Int64) String() string

type Invokable added in v0.1.2

type Invokable interface {
	Value
	Invoke(scope Scope, args ...Value) (Value, error)
}

Invokable represents any value that supports invocation. Vector, Fn etc support invocation.

type Keyword added in v0.1.2

type Keyword string

Keyword represents a keyword literal.

func (Keyword) Eval added in v0.1.2

func (kw Keyword) Eval(_ Scope) (Value, error)

Eval simply returns itself since Keywords evaluate to themselves.

func (Keyword) String added in v0.1.2

func (kw Keyword) String() string

type List added in v0.1.2

type List struct {
	Values
	Position
	// contains filtered or unexported fields
}

List represents an list of forms/vals. Evaluating a list leads to a function invocation.

func (*List) Eval added in v0.1.2

func (lf *List) Eval(scope Scope) (Value, error)

Eval performs an invocation.

func (List) String added in v0.1.2

func (lf List) String() string

type MapScope

type MapScope struct {
	// contains filtered or unexported fields
}

MapScope implements Scope using a Go native hash-map.

func New

func New() *MapScope

New initializes a new scope with all the core bindings.

func NewScope added in v0.1.2

func NewScope(parent Scope) *MapScope

NewScope returns an instance of MapScope with no bindings. If you need builtin special forms, pass result of New() as argument.

func (*MapScope) Bind

func (scope *MapScope) Bind(symbol string, v Value) error

Bind adds the given value to the scope and binds the symbol to it.

func (*MapScope) BindGo added in v0.1.2

func (scope *MapScope) BindGo(symbol string, v interface{}) error

BindGo is similar to Bind but handles conversion of Go value 'v' to sabre Value type. See `ValueOf()`

func (*MapScope) Parent added in v0.1.3

func (scope *MapScope) Parent() Scope

Parent returns the parent scope of this scope.

func (*MapScope) Resolve

func (scope *MapScope) Resolve(symbol string) (Value, error)

Resolve finds the value bound to the given symbol and returns it if found in this scope or parent scope if any. Returns error otherwise.

type Module added in v0.1.2

type Module []Value

Module represents a group of forms. Evaluating a module leads to evaluation of each form in order and result will be the result of last evaluation.

func (Module) Compare added in v0.3.1

func (mod Module) Compare(v Value) bool

Compare returns true if the 'v' is also a module and all forms in the module are equivalent.

func (Module) Eval added in v0.1.2

func (mod Module) Eval(scope Scope) (Value, error)

Eval evaluates all the vals in the module body and returns the result of the last evaluation.

func (Module) String added in v0.1.2

func (mod Module) String() string

type MultiFn added in v0.1.3

type MultiFn struct {
	Name    string
	IsMacro bool
	Methods []Fn
}

MultiFn represents a multi-arity function or macro definition.

func (MultiFn) Compare added in v0.3.1

func (multiFn MultiFn) Compare(v Value) bool

Compare returns true if 'v' is also a MultiFn and all methods are equivalent.

func (MultiFn) Eval added in v0.1.3

func (multiFn MultiFn) Eval(_ Scope) (Value, error)

Eval returns the multiFn definition itself.

func (MultiFn) Expand added in v0.3.0

func (multiFn MultiFn) Expand(scope Scope, args []Value) (Value, error)

Expand executes the macro body and returns the result of the expansion.

func (MultiFn) Invoke added in v0.1.3

func (multiFn MultiFn) Invoke(scope Scope, args ...Value) (Value, error)

Invoke dispatches the call to a method based on number of arguments.

func (MultiFn) String added in v0.1.3

func (multiFn MultiFn) String() string

type Nil added in v0.1.2

type Nil struct{}

Nil represents a nil value.

func (Nil) Eval added in v0.1.2

func (n Nil) Eval(_ Scope) (Value, error)

Eval returns the underlying value.

func (Nil) String added in v0.1.2

func (n Nil) String() string

type Position added in v0.2.0

type Position struct {
	File   string
	Line   int
	Column int
}

Position represents the positional information about a value read by reader.

func (Position) GetPos added in v0.2.0

func (pi Position) GetPos() (file string, line, col int)

GetPos returns the file, line and column values.

func (*Position) SetPos added in v0.3.1

func (pi *Position) SetPos(file string, line, col int)

SetPos sets the position information.

func (Position) String added in v0.2.0

func (pi Position) String() string

type ReadError added in v0.1.2

type ReadError struct {
	Position
	Cause  error
	Messag string
}

ReadError wraps the parsing/eval errors with relevant information.

func (ReadError) Error added in v0.1.2

func (err ReadError) Error() string

func (ReadError) Unwrap added in v0.1.2

func (err ReadError) Unwrap() error

Unwrap returns underlying cause of the error.

type Reader added in v0.1.2

type Reader struct {
	File string
	// contains filtered or unexported fields
}

Reader provides functions to parse characters from a stream into symbolic expressions or forms.

func NewReader added in v0.1.2

func NewReader(rs io.Reader) *Reader

NewReader returns a lisp reader instance which can read forms from rs. Reader behavior can be customized by using SetMacro to override or remove from the default read table. File name will be inferred from the reader value and type information or can be set manually on the Reader.

func (*Reader) All added in v0.1.2

func (rd *Reader) All() (Value, error)

All consumes characters from stream until EOF and returns a list of all the forms parsed. Any no-op forms (e.g., comment) returned will not be included in the result.

func (*Reader) IsTerminal added in v0.1.2

func (rd *Reader) IsTerminal(r rune) bool

IsTerminal returns true if the rune should terminate a form. ReaderMacro trigger runes defined in the read table and all space characters including "," are considered terminal.

func (*Reader) NextRune added in v0.1.2

func (rd *Reader) NextRune() (rune, error)

NextRune returns next rune from the stream and advances the stream.

func (*Reader) One added in v0.1.2

func (rd *Reader) One() (Value, error)

One consumes characters from underlying stream until a complete form is parsed and returns the form while ignoring the no-op forms like comments. Except EOF, all errors will be wrapped with ReaderError type along with the positional information obtained using Position().

func (Reader) Position added in v0.2.0

func (rd Reader) Position() Position

Position returns information about the stream including file name and the position of the reader.

func (*Reader) SetMacro added in v0.1.2

func (rd *Reader) SetMacro(init rune, macro ReaderMacro, isDispatch bool)

SetMacro sets the given reader macro as the handler for init rune in the read table. Overwrites if a macro is already present. If the macro value given is nil, entry for the init rune will be removed from the read table. isDispatch decides if the macro is a dispatch macro and takes effect only after a '#' sign.

func (*Reader) SkipSpaces added in v0.1.2

func (rd *Reader) SkipSpaces() error

SkipSpaces consumes and discards runes from stream repeatedly until a character that is not a whitespace is identified. Along with standard unicode white-space characters "," is also considered a white-space and discarded.

func (*Reader) Unread added in v0.1.2

func (rd *Reader) Unread(runes ...rune)

Unread can be used to return runes consumed from the stream back to the stream. Un-reading more runes than read is guaranteed to work but might cause inconsistency in stream positional information.

type ReaderMacro added in v0.1.2

type ReaderMacro func(rd *Reader, init rune) (Value, error)

ReaderMacro implementations can be plugged into the Reader to extend, override or customize behavior of the reader.

type Scope

type Scope interface {
	Parent() Scope
	Bind(symbol string, v Value) error
	Resolve(symbol string) (Value, error)
}

Scope implementation is responsible for managing value bindings.

type Seq added in v0.1.3

type Seq interface {
	Value
	// First should return first value of the sequence or nil if the
	// sequence is empty.
	First() Value
	// Next should return the remaining sequence when the first value
	// is excluded.
	Next() Seq
	// Cons should add the value to the beginning of the sequence and
	// return the new sequence.
	Cons(v Value) Seq
	// Conj should join the given values to the sequence and return a
	// new sequence.
	Conj(vals ...Value) Seq
}

Seq implementations represent a sequence/list of values.

type Set added in v0.1.2

type Set struct {
	Values
	Position
}

Set represents a list of unique values. (Experimental)

func (Set) Eval added in v0.1.2

func (set Set) Eval(scope Scope) (Value, error)

Eval evaluates each value in the set form and returns the resultant values as new set.

func (Set) String added in v0.1.2

func (set Set) String() string

type SpecialForm added in v0.2.3

type SpecialForm struct {
	Name  string
	Parse func(scope Scope, args []Value) (*Fn, error)
}

SpecialForm is a Value type for representing special forms that will be subjected to an intermediate Parsing stage before evaluation.

func (SpecialForm) Eval added in v0.2.3

func (sf SpecialForm) Eval(_ Scope) (Value, error)

Eval always returns error since it is not allowed to directly evaluate a special form.

func (SpecialForm) String added in v0.2.3

func (sf SpecialForm) String() string

type String added in v0.1.2

type String string

String represents double-quoted string literals. String Form represents the true string value obtained from the reader. Escape sequences are not applicable at this level.

func (String) Conj added in v0.2.0

func (se String) Conj(vals ...Value) Seq

Conj joins the given values to list of characters of the string and returns the new sequence.

func (String) Cons added in v0.2.0

func (se String) Cons(v Value) Seq

Cons converts the string to character sequence and adds the given value to the beginning of the list.

func (String) Eval added in v0.1.2

func (se String) Eval(_ Scope) (Value, error)

Eval simply returns itself since Strings evaluate to themselves.

func (String) First added in v0.2.0

func (se String) First() Value

First returns the first character if string is not empty, nil otherwise.

func (String) Next added in v0.2.0

func (se String) Next() Seq

Next slices the string by excluding first character and returns the remainder.

func (String) String added in v0.1.2

func (se String) String() string

type Symbol added in v0.1.2

type Symbol struct {
	Position
	Value string
}

Symbol represents a name given to a value in memory.

func (Symbol) Compare added in v0.2.3

func (sym Symbol) Compare(v Value) bool

Compare compares this symbol to the given value. Returns true if the given value is a symbol with same data.

func (Symbol) Eval added in v0.1.2

func (sym Symbol) Eval(scope Scope) (Value, error)

Eval returns the value bound to this symbol in current context. If the symbol is in fully qualified form (i.e., separated by '.'), eval does recursive member access.

func (Symbol) String added in v0.1.2

func (sym Symbol) String() string

type Type added in v0.2.2

type Type struct{ T reflect.Type }

Type represents the type value of a given value. Type also implements Value type.

func (Type) Eval added in v0.2.2

func (t Type) Eval(_ Scope) (Value, error)

Eval returns the type value itself.

func (Type) Invoke added in v0.2.2

func (t Type) Invoke(scope Scope, args ...Value) (Value, error)

Invoke creates zero value of the given type.

func (Type) String added in v0.2.2

func (t Type) String() string

type Value

type Value interface {
	// String should return the LISP representation of the value.
	String() string
	// Eval should evaluate this value against the scope and return
	// the resultant value or an evaluation error.
	Eval(scope Scope) (Value, error)
}

Value represents data/forms in sabre. This includes those emitted by Reader, values obtained as result of an evaluation etc.

func Eval

func Eval(scope Scope, form Value) (Value, error)

Eval evaluates the given form against the scope and returns the result of evaluation.

func MacroExpand added in v0.3.2

func MacroExpand(scope Scope, form Value) (Value, bool, error)

MacroExpand expands the macro invocation form.

func ReadEval added in v0.1.3

func ReadEval(scope Scope, r io.Reader) (Value, error)

ReadEval consumes data from reader 'r' till EOF, parses into forms and evaluates all the forms obtained and returns the result.

func ReadEvalStr added in v0.1.3

func ReadEvalStr(scope Scope, src string) (Value, error)

ReadEvalStr is a convenience wrapper for Eval that reads forms from string and evaluates for result.

func ValueOf added in v0.1.2

func ValueOf(v interface{}) Value

ValueOf converts a Go value to sabre Value type. If 'v' is already a Value type, it is returned as is. Primitive Go values like string, rune, int, float, bool are converted to the right sabre Value types. Functions are converted to the wrapper 'Fn' type. Value of type 'reflect.Type' will be wrapped as 'Type' which enables initializing a value of that type when invoked. All other types will be wrapped using 'Any' type.

type Values added in v0.1.3

type Values []Value

Values represents a list of values and implements the Seq interface.

func (Values) Compare added in v0.2.1

func (vals Values) Compare(v Value) bool

Compare compares the values in this sequence to the other sequence. other sequence will be realized for comparison.

func (Values) Conj added in v0.2.0

func (vals Values) Conj(args ...Value) Seq

Conj returns a new sequence where 'v' is appended to the values.

func (Values) Cons added in v0.1.3

func (vals Values) Cons(v Value) Seq

Cons returns a new sequence where 'v' is prepended to the values.

func (Values) Eval added in v0.2.0

func (vals Values) Eval(_ Scope) (Value, error)

Eval returns itself.

func (Values) First added in v0.1.3

func (vals Values) First() Value

First returns the first value in the list if the list is not empty. Returns Nil{} otherwise.

func (Values) Next added in v0.1.3

func (vals Values) Next() Seq

Next returns a new sequence containing values after the first one. If there are no values to create a next sequence, returns nil.

func (Values) Size added in v0.1.3

func (vals Values) Size() int

Size returns the number of items in the list.

func (Values) String added in v0.2.0

func (vals Values) String() string

func (Values) Uniq added in v0.2.0

func (vals Values) Uniq() []Value

Uniq removes all the duplicates from the given value array. TODO: remove this naive implementation

type Vector added in v0.1.2

type Vector struct {
	Values
	Position
}

Vector represents a list of values. Unlike List type, evaluation of vector does not lead to function invoke.

func (Vector) Eval added in v0.1.2

func (vf Vector) Eval(scope Scope) (Value, error)

Eval evaluates each value in the vector form and returns the resultant values as new vector.

func (Vector) Invoke added in v0.1.2

func (vf Vector) Invoke(scope Scope, args ...Value) (Value, error)

Invoke of a vector performs a index lookup. Only arity 1 is allowed and should be an integer value to be used as index.

func (Vector) String added in v0.1.2

func (vf Vector) String() string

Directories

Path Synopsis
examples
Package repl provides a REPL implementation and options to expose Sabre features through a read-eval-print-loop.
Package repl provides a REPL implementation and options to expose Sabre features through a read-eval-print-loop.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL