From 9c9b4f9c875ba8aa0dcdd70d4d1355606fe5fbcc Mon Sep 17 00:00:00 2001 From: Roberto Vidal Date: Tue, 2 Jul 2024 17:02:03 +0200 Subject: [PATCH] more methods: ports, chars (#227) * more methods: ports, chars * support aliases for native definitions * support arbitrary tokens in native definition docs * aliases, more docs, minor fixes * rename raw functions * minor changes and additions * minor changes * update docs * fix infinite loop --- cogs/r5rs.scm | 15 + cogs/r7rs.scm | 24 + .../steel-core/src/primitives/bytevectors.rs | 67 ++- crates/steel-core/src/primitives/lists.rs | 1 + crates/steel-core/src/primitives/ports.rs | 245 ++++++++- crates/steel-core/src/primitives/strings.rs | 475 +++++++++++++----- crates/steel-core/src/primitives/time.rs | 2 +- .../src/scheme/modules/parameters.scm | 20 +- crates/steel-core/src/steel_vm/builtin.rs | 38 +- .../src/steel_vm/const_evaluation.rs | 2 +- crates/steel-core/src/steel_vm/primitives.rs | 17 +- crates/steel-core/src/steel_vm/vm.rs | 2 +- crates/steel-core/src/values/json_vals.rs | 4 +- crates/steel-core/src/values/port.rs | 317 +++++++----- crates/steel-core/src/values/structs.rs | 58 ++- crates/steel-derive/src/lib.rs | 125 +++-- crates/steel-doc/src/main.rs | 4 +- crates/steel-parser/src/lexer.rs | 10 + docs/src/builtins/steel_base.md | 262 ++++++++-- docs/src/builtins/steel_bytevectors.md | 22 +- docs/src/builtins/steel_identity.md | 5 + docs/src/builtins/steel_json.md | 4 +- docs/src/builtins/steel_lists.md | 2 - docs/src/builtins/steel_ports.md | 90 +++- docs/src/builtins/steel_strings.md | 151 +++++- 25 files changed, 1531 insertions(+), 431 deletions(-) diff --git a/cogs/r5rs.scm b/cogs/r5rs.scm index 69984e8b4..08bd887f5 100644 --- a/cogs/r5rs.scm +++ b/cogs/r5rs.scm @@ -437,6 +437,21 @@ (check-equal? "case-insensitive string >=, true" #t (string-ci>=? "aa" "A")) (check-equal? "case-insensitive string >=, same string" #t (string-ci>=? "a" "A")) +(check-equal? "char=?, true" #t (char=? #\a #\a #\a)) +(check-equal? "char=?, false" #f (char=? #\a #\A)) +(check-equal? "char?, false" #f (char>? #\a #\b)) +(check-equal? "char>?, false, strict" #f (char>? #\a #\a)) +(check-equal? "char>?, true" #t (char>? #\c #\b #\a)) +(check-equal? "char<=?, true" #t (char<=? #\a #\b #\b)) +(check-equal? "char<=?, true, non-strict" #t (char<=? #\a #\a)) +(check-equal? "char<=?, false" #f (char<=? #\b #\a)) +(check-equal? "char>=?, false" #f (char>=? #\a #\b)) +(check-equal? "char>=?, true, non-strict" #t (char>=? #\a #\a)) +(check-equal? "char>=?, true" #t (char>=? #\b #\b #\a)) + (check-equal? "make-string creates single character string 'a' correctly" #t (string=? "a" (make-string 1 #\a))) diff --git a/cogs/r7rs.scm b/cogs/r7rs.scm index f04878a5b..fb00d7274 100644 --- a/cogs/r7rs.scm +++ b/cogs/r7rs.scm @@ -70,6 +70,30 @@ (parameterize ([location "on a bus"]) (would-you-could-you?))) +;; Bytevectors + +;; TODO: use bytevector literals +(check-equal? "utf8->string" "ABC" (utf8->string (bytes #x41 #x42 #x43))) +(check-equal? "utf8->string, multi-byte char" "λ" (utf8->string (bytes #xCE #xBB))) +(check-equal? "utf8->string with start" "ABC" (utf8->string (bytes 0 #x41 #x42 #x43) 1)) +(check-equal? "utf8->string with start and end" "ABC" (utf8->string (bytes 0 #x41 #x42 #x43 0) 1 4)) +(check-equal? "utf8->string with start and end, multi-byte char" "λ" (utf8->string (bytes 0 #xCE #xBB 0) 1 3)) +(check-equal? "string->utf8" (bytes #x41 #x42 #x43) (string->utf8 "ABC")) +(check-equal? "string->utf8 with start" (bytes #x42 #x43) (string->utf8 "ABC" 1)) +(check-equal? "string->utf8 with start and end" (bytes #x42) (string->utf8 "ABC" 1 2)) +(check-equal? "string->utf8 with start and end, multi-byte" (bytes #xCE #xBB) (string->utf8 "σλC" 1 2)) +(check-equal? "string->utf8, multi-byte char" (bytes #xCE #xBB) (string->utf8 "λ")) + +(check-equal? "char->integer, special escape, null" 0 (char->integer (read (open-input-string "#\\null")))) +(check-equal? "char->integer, special escape, alarm" 7 (char->integer (read (open-input-string "#\\alarm")))) +(check-equal? "char->integer, special escape, backspace" 8 (char->integer (read (open-input-string "#\\backspace")))) +(check-equal? "char->integer, special escape, tab" 9 (char->integer (read (open-input-string "#\\tab")))) +(check-equal? "char->integer, special escape, newline" 10 (char->integer (read (open-input-string "#\\newline")))) +(check-equal? "char->integer, special escape, return" 13 (char->integer (read (open-input-string "#\\return")))) +(check-equal? "char->integer, special escape, delete" #x7F (char->integer (read (open-input-string "#\\delete")))) +(check-equal? "char->integer, special escape, escape" #x1B (char->integer (read (open-input-string "#\\escape")))) +(check-equal? "char->integer, multi-byte" #x03BB (char->integer (read (open-input-string "#\\λ")))) + (define r7rs-test-stats (get-test-stats)) (displayln "Passed: " (hash-ref r7rs-test-stats 'success-count)) diff --git a/crates/steel-core/src/primitives/bytevectors.rs b/crates/steel-core/src/primitives/bytevectors.rs index 4c09c0752..83e684e9e 100644 --- a/crates/steel-core/src/primitives/bytevectors.rs +++ b/crates/steel-core/src/primitives/bytevectors.rs @@ -3,7 +3,7 @@ use steel_derive::function; use crate::{ rerrs::ErrorKind, rvals::FromSteelVal, - rvals::{RestArgsIter, Result, SteelByteVector, SteelString}, + rvals::{RestArgsIter, Result, SteelByteVector}, steel_vm::builtin::BuiltInModule, stop, throw, SteelErr, SteelVal, }; @@ -20,12 +20,12 @@ pub fn bytevector_module() -> BuiltInModule { .register_native_fn_definition(MAKE_BYTES_DEFINITION) .register_native_fn_definition(IS_BYTE_DEFINITION) .register_native_fn_definition(BYTES_LENGTH_DEFINITION) - .register_native_fn_definition(STRING_TO_BYTES_DEFINITION) .register_native_fn_definition(BYTES_REF_DEFINITION) .register_native_fn_definition(BYTES_SET_DEFINITION) .register_native_fn_definition(BYTES_TO_LIST_DEFINITION) .register_native_fn_definition(LIST_TO_BYTES_DEFINITION) - .register_native_fn_definition(BYTES_APPEND_DEFINITION); + .register_native_fn_definition(BYTES_APPEND_DEFINITION) + .register_native_fn_definition(BYTES_TO_STRING_DEFINITION); module } @@ -206,19 +206,6 @@ pub fn bytes_length(value: &SteelByteVector) -> usize { value.vec.borrow().len() } -/// Converts the given string to a bytevector -/// -/// # Examples -/// ```scheme -/// (string->bytes "Apple") ;; => (bytes 65 112 112 108 101) -/// ``` -#[function(name = "string->bytes")] -pub fn string_to_bytes(value: &SteelString) -> Result { - Ok(SteelVal::ByteVector(SteelByteVector::new( - value.as_str().as_bytes().to_vec(), - ))) -} - /// Fetches the byte at the given index within the bytevector. /// If the index is out of bounds, this will error. /// @@ -320,3 +307,51 @@ pub fn bytes_append(value: &SteelByteVector, other: &SteelByteVector) -> Result< .collect(), ))) } + +/// Decodes a string from a bytevector containing valid UTF-8. +/// +/// (bytes->string/utf8 buf [start] [end]) -> string? +/// +/// * buf : bytes? +/// * start: int? = 0 +/// * end: int? = (bytes-length buf) +/// +/// # Examples +/// ```scheme +/// (bytes->string/utf8 (bytes #xe5 #x8d #x83 #xe8 #x91 #x89)) ;; => "千葉" +/// ``` +#[function(name = "bytes->string/utf8", alias = "utf8->string")] +pub fn bytes_to_string( + value: &SteelByteVector, + mut rest: RestArgsIter<'_, isize>, +) -> Result { + let borrowed = (&*value.vec).borrow(); + + let start = rest.next().transpose()?.unwrap_or(0); + let end = rest.next().transpose()?.unwrap_or(borrowed.len() as isize); + + if rest.next().is_some() { + stop!(ArityMismatch => "expected at most 3 arguments"); + } + + if start < 0 { + stop!(ContractViolation => "start should be a positive number, got {}", start); + } + + if end < 0 { + stop!(ContractViolation => "end should be a positive number, got {}", end); + } + + if end < start { + stop!(ContractViolation => "start should be smaller than end, got {} and {}", start, end); + } + + let start = start as usize; + let end = end as usize; + + let Ok(s) = std::str::from_utf8(&(&*borrowed)[start..end]) else { + stop!(ConversionError => "bytevector contains malformed UTF-8") + }; + + Ok(s.to_string().into()) +} diff --git a/crates/steel-core/src/primitives/lists.rs b/crates/steel-core/src/primitives/lists.rs index 984f6d373..111c0f869 100644 --- a/crates/steel-core/src/primitives/lists.rs +++ b/crates/steel-core/src/primitives/lists.rs @@ -83,6 +83,7 @@ pub fn list_module() -> BuiltInModule { .register_native_fn_definition(PAIR_DEFINITION) .register_native_fn_definition(NativeFunctionDefinition { name: "apply", + aliases: &[], func: BuiltInFunctionType::Context(apply), arity: Arity::Exact(2), doc: Some(APPLY_DOC), diff --git a/crates/steel-core/src/primitives/ports.rs b/crates/steel-core/src/primitives/ports.rs index 6f9c2f3c0..69cb68718 100644 --- a/crates/steel-core/src/primitives/ports.rs +++ b/crates/steel-core/src/primitives/ports.rs @@ -1,13 +1,17 @@ -use crate::rvals::{Result, SteelString, SteelVal}; +use crate::rvals::{RestArgsIter, Result, SteelByteVector, SteelString, SteelVal}; use crate::steel_vm::builtin::BuiltInModule; use crate::stop; use crate::values::port::new_rc_ref_cell; use crate::values::port::{SteelPort, SteelPortRepr}; +use crate::values::structs::{make_struct_singleton, StructTypeDescriptor}; +use once_cell::unsync::Lazy; use steel_derive::function; thread_local! { - pub static EOF_OBJECT: SteelString = "eof".into(); + static EOF_OBJECT: Lazy<(SteelVal, StructTypeDescriptor)>= Lazy::new(|| { + make_struct_singleton("eof".into()) + }); } pub fn port_module() -> BuiltInModule { @@ -18,6 +22,7 @@ pub fn port_module() -> BuiltInModule { .register_native_fn_definition(OPEN_INPUT_FILE_DEFINITION) .register_native_fn_definition(OPEN_OUTPUT_FILE_DEFINITION) .register_native_fn_definition(OPEN_OUTPUT_STRING_DEFINITION) + .register_native_fn_definition(OPEN_OUTPUT_BYTEVECTOR_DEFINITION) .register_native_fn_definition(WRITE_LINE_DEFINITION) .register_native_fn_definition(WRITE_STRING_DEFINITION) .register_native_fn_definition(WRITE_DEFINITION) @@ -26,13 +31,26 @@ pub fn port_module() -> BuiltInModule { .register_native_fn_definition(READ_PORT_TO_STRING_DEFINITION) .register_native_fn_definition(READ_LINE_TO_STRING_DEFINITION) .register_native_fn_definition(GET_OUTPUT_STRING_DEFINITION) + .register_native_fn_definition(GET_OUTPUT_BYTEVECTOR_DEFINITION) .register_native_fn_definition(IS_INPUT_DEFINITION) .register_native_fn_definition(IS_OUTPUT_DEFINITION) .register_native_fn_definition(DEFAULT_INPUT_PORT_DEFINITION) - .register_native_fn_definition(DEFAULT_OUTPUT_PORT_DEFINITION); + .register_native_fn_definition(DEFAULT_OUTPUT_PORT_DEFINITION) + .register_native_fn_definition(CLOSE_OUTPUT_PORT_DEFINITION) + .register_native_fn_definition(DEFAULT_ERROR_PORT_DEFINITION) + .register_native_fn_definition(EOF_OBJECT_DEFINITION) + .register_native_fn_definition(OPEN_INPUT_STRING_DEFINITION) + .register_native_fn_definition(OPEN_INPUT_BYTEVECTOR_DEFINITION) + .register_native_fn_definition(READ_BYTE_DEFINITION) + .register_native_fn_definition(READ_CHAR_DEFINITION) + .register_native_fn_definition(WRITE_BYTE_DEFINITION) + .register_native_fn_definition(WRITE_BYTES_DEFINITION) + .register_native_fn_definition(PEEK_BYTE_DEFINITION); module } +// TODO: implement textual-port? and binary-port? + /// Gets the port handle to stdin /// /// (stdin) -> input-port? @@ -89,11 +107,62 @@ pub fn open_output_file(path: &SteelString) -> Result { SteelPort::new_textual_file_output(path).map(SteelVal::PortV) } +/// Creates an output port that accumulates what is written into a string. +/// This string can be recovered calling `get-output-string`. +/// +/// (open-output-string) -> output-port? +/// +/// # Examples +/// ```scheme +/// (define out (open-output-string)) +/// +/// +/// (write-char "α" out) +/// (write-char "ω" out) +/// +/// (get-output-string out) ;; => "αω" +/// ``` #[function(name = "open-output-string")] pub fn open_output_string() -> SteelVal { - SteelVal::PortV(SteelPort::new_output_port()) + SteelVal::PortV(SteelPort::new_output_port_string()) +} + +/// Creates an output port that accumulates what is written into a bytevector. +/// These bytes can be recovered calling `get-output-bytevector`. +/// +/// (open-output-bytevector) -> output-port? +/// +/// # Examples +/// ```scheme +/// (define out (open-output-bytevector)) +/// +/// +/// (write-byte 30 out) +/// (write-byte 250 out) +/// +/// (get-output-bytevector out) ;; => (bytes 30 250) +/// ``` +#[function(name = "open-output-bytevector")] +pub fn open_output_bytevector() -> SteelVal { + SteelVal::PortV(SteelPort::new_output_port_string()) +} + +/// Creates an input port from a string, that will return the string contents. +/// +/// (open-input-string string?) -> input-port? +#[function(name = "open-input-string")] +pub fn open_input_string(s: &SteelString) -> SteelVal { + SteelVal::PortV(SteelPort::new_input_port_string(s.to_string())) } +/// Creates an input port from a bytevector, that will return the bytevector contents. +/// +/// (open-input-bytevector bytes?) -> input-port? +#[function(name = "open-input-bytevector")] +pub fn open_input_bytevector(bytes: &SteelByteVector) -> SteelVal { + let vec: &Vec = &*bytes.vec.borrow(); + SteelVal::PortV(SteelPort::new_input_port_bytevector(vec.clone())) +} /// Takes a port and reads the entire content into a string /// /// (read-port-to-string port) -> string? @@ -149,7 +218,7 @@ pub fn read_line_to_string(port: &SteelPort) -> Result { if let Ok((size, result)) = res { if size == 0 { - Ok(SteelVal::SymbolV(EOF_OBJECT.with(|x| x.clone()))) + Ok(eof()) } else { Ok(SteelVal::StringV(result.into())) } @@ -171,10 +240,11 @@ pub fn write_line(port: &SteelPort, line: &SteelVal) -> Result { } } -#[function(name = "raw-write")] -pub fn write(port: &SteelPort, line: &SteelVal) -> Result { +#[function(name = "#%raw-write")] +pub fn write(line: &SteelVal, rest: RestArgsIter<&SteelPort>) -> Result { + let port = output_args(rest)?; let line = line.to_string(); - let res = port.write_string(line.as_str()); + let res = port.write(line.as_str().as_bytes()); if res.is_ok() { Ok(SteelVal::Void) @@ -183,8 +253,9 @@ pub fn write(port: &SteelPort, line: &SteelVal) -> Result { } } -#[function(name = "raw-write-char")] -pub fn write_char(port: &SteelPort, character: char) -> Result { +#[function(name = "#%raw-write-char")] +pub fn write_char(character: char, rest: RestArgsIter<&SteelPort>) -> Result { + let port = output_args(rest)?; let res = port.write_char(character); if res.is_ok() { @@ -194,12 +265,15 @@ pub fn write_char(port: &SteelPort, character: char) -> Result { } } -#[function(name = "raw-write-string")] -pub fn write_string(port: &SteelPort, line: &SteelVal) -> Result { +// TODO: support start and end +#[function(name = "#%raw-write-string")] +pub fn write_string(line: &SteelVal, rest: RestArgsIter<&SteelPort>) -> Result { + let port = output_args(rest)?; + let res = if let SteelVal::StringV(s) = line { - port.write_string(s.as_str()) + port.write(s.as_str().as_bytes()) } else { - port.write_string(line.to_string().as_str()) + port.write(line.to_string().as_str().as_bytes()) }; if res.is_ok() { @@ -209,9 +283,32 @@ pub fn write_string(port: &SteelPort, line: &SteelVal) -> Result { } } +/// Extracts the string contents from a port created with `open-output-string`. +/// +/// (get-output-string port?) -> string? #[function(name = "get-output-string")] pub fn get_output_string(port: &SteelPort) -> Result { - port.get_output_string().map(SteelVal::from) + let Some(bytes) = port.get_output()? else { + stop!(TypeMismatch => "get-output-string expects an output port created with open-output-string"); + }; + + let Ok(s) = String::from_utf8(bytes) else { + stop!(Generic => "Port contents are not valid UTF-8"); + }; + + Ok(s.into()) +} + +/// Extracts the contents from a port created with `open-output-bytevector`. +/// +/// (get-output-bytevector port?) -> bytes? +#[function(name = "get-output-bytevector")] +pub fn get_output_bytevector(port: &SteelPort) -> Result { + let Some(bytes) = port.get_output()? else { + stop!(TypeMismatch => "get-output-bytevector expects an output port created with open-output-bytevector"); + }; + + Ok(SteelVal::ByteVector(SteelByteVector::new(bytes))) } #[function(name = "flush-output-port")] @@ -229,9 +326,123 @@ pub fn default_output_port() -> SteelVal { SteelVal::PortV(SteelPort::default_current_output_port()) } -// TODO: In order for this to work, ports have to get refactored - the mutability needs to be -// on the outside, rather than the inside. +#[function(name = "#%default-error-port")] +pub fn default_error_port() -> SteelVal { + SteelVal::PortV(SteelPort::default_current_error_port()) +} + #[function(name = "close-output-port")] pub fn close_output_port(port: &SteelPort) -> Result { port.close_output_port().map(|_| SteelVal::Void) } + +/// Returns `#t` if the value is an EOF object. +/// +/// (eof-object? any/c) -> bool? +#[function(name = "eof-object?")] +pub fn eof_objectp(value: &SteelVal) -> bool { + let SteelVal::CustomStruct(struct_) = value else { + return false; + }; + + EOF_OBJECT.with(|eof| struct_.type_descriptor == eof.1) +} + +/// Returns an EOF object. +/// +/// (eof-object) -> eof-object? +#[function(name = "eof-object")] +pub fn eof_object() -> SteelVal { + eof() +} + +/// Reads a single byte from an input port. +/// +/// (read-byte [port]) -> byte? +/// +/// * port : input-port? = (current-input-port) +#[function(name = "read-byte")] +pub fn read_byte(rest: RestArgsIter<&SteelPort>) -> Result { + let port = input_args(rest)?; + + Ok(port + .read_byte()? + .map(|b| SteelVal::IntV(b.into())) + .unwrap_or_else(eof)) +} + +/// Writes a single byte to an output port. +/// +/// (write-byte b [port]) +/// +/// * b : byte? +/// * port : output-port? = (current-output-port) +#[function(name = "write-byte")] +pub fn write_byte(byte: u8, rest: RestArgsIter<&SteelPort>) -> Result { + let port = output_args(rest)?; + port.write(&[byte])?; + + Ok(SteelVal::Void) +} + +/// Writes the contents of a bytevector into an output port. +/// +/// (write-bytes buf [port]) +/// +/// * buf : bytes? +/// * port : output-port? = (current-output-port) +#[function(name = "write-bytes")] +pub fn write_bytes(bytes: &SteelByteVector, rest: RestArgsIter<&SteelPort>) -> Result { + let port = output_args(rest)?; + port.write(&*bytes.vec.borrow())?; + + Ok(SteelVal::Void) +} + +/// Peeks the next byte from an input port. +/// +/// (peek-byte [port]) -> byte? +/// +/// * port : input-port? = (current-input-port) +#[function(name = "peek-byte")] +pub fn peek_byte(rest: RestArgsIter<&SteelPort>) -> Result { + let port = input_args(rest)?; + + Ok(port + .peek_byte()? + .map(|b| SteelVal::IntV(b.into())) + .unwrap_or_else(eof)) +} + +/// Reads the next character from an input port. +/// +/// (read-char [port]) -> char? +/// +/// * port : input-port? = (current-input-port) +#[function(name = "read-char")] +pub fn read_char(rest: RestArgsIter<&SteelPort>) -> Result { + let port = input_args(rest)?; + Ok(port.read_char()?.map(SteelVal::CharV).unwrap_or_else(eof)) +} + +fn input_args(args: RestArgsIter<&SteelPort>) -> Result { + Ok(io_args(1, args)?.unwrap_or_else(SteelPort::default_current_input_port)) +} + +fn output_args(args: RestArgsIter<&SteelPort>) -> Result { + Ok(io_args(2, args)?.unwrap_or_else(SteelPort::default_current_output_port)) +} + +fn io_args(max: usize, mut args: RestArgsIter<&SteelPort>) -> Result> { + let port = args.next().transpose()?.cloned(); + + if args.next().is_some() { + stop!(ArityMismatch => "expected at most {} arguments", max); + } + + Ok(port) +} + +pub fn eof() -> SteelVal { + EOF_OBJECT.with(|eof| eof.0.clone()) +} diff --git a/crates/steel-core/src/primitives/strings.rs b/crates/steel-core/src/primitives/strings.rs index f030e56c5..b4bff29bc 100644 --- a/crates/steel-core/src/primitives/strings.rs +++ b/crates/steel-core/src/primitives/strings.rs @@ -1,18 +1,13 @@ use crate::gc::Gc; use crate::values::lists::List; -use crate::rvals::{RestArgsIter, Result, SteelString, SteelVal}; +use crate::rvals::{RestArgsIter, Result, SteelByteVector, SteelString, SteelVal}; use crate::steel_vm::builtin::BuiltInModule; -use crate::steel_vm::register_fn::RegisterFn; use crate::stop; use num::{BigInt, Num}; use steel_derive::{function, native}; -fn char_upcase(c: char) -> char { - c.to_ascii_uppercase() -} - /// Strings in Steel are immutable, fixed length arrays of characters. They are heap allocated, and /// are implemented under the hood as referenced counted Rust `Strings`. Rust `Strings` are stored /// as UTF-8 encoded bytes. @@ -56,21 +51,59 @@ pub fn string_module() -> BuiltInModule { .register_native_fn_definition(STRING_TO_NUMBER_DEFINITION) .register_native_fn_definition(NUMBER_TO_STRING_DEFINITION) .register_native_fn_definition(REPLACE_DEFINITION) - .register_fn("char-upcase", char_upcase) - .register_fn("char-whitespace?", char::is_whitespace) - .register_fn("char-digit?", |c: char| char::is_digit(c, 10)) - .register_fn("char->number", |c: char| char::to_digit(c, 10)) - .register_native_fn_definition(CHAR_EQUALS_DEFINITION); + .register_native_fn_definition(CHAR_UPCASE_DEFINITION) + .register_native_fn_definition(CHAR_DOWNCASE_DEFINITION) + .register_native_fn_definition(CHAR_IS_DIGIT_DEFINITION) + .register_native_fn_definition(CHAR_IS_WHITESPACE_DEFINITION) + .register_native_fn_definition(CHAR_TO_NUMBER_DEFINITION) + .register_native_fn_definition(CHAR_EQUALS_DEFINITION) + .register_native_fn_definition(CHAR_GREATER_THAN_DEFINITION) + .register_native_fn_definition(CHAR_GREATER_THAN_EQUAL_TO_DEFINITION) + .register_native_fn_definition(CHAR_LESS_THAN_DEFINITION) + .register_native_fn_definition(CHAR_LESS_THAN_EQUAL_TO_DEFINITION) + .register_native_fn_definition(CHAR_TO_INTEGER_DEFINITION) + .register_native_fn_definition(INTEGER_TO_CHAR_DEFINITION) + .register_native_fn_definition(STRING_TO_BYTES_DEFINITION) + .register_native_fn_definition(STRING_TO_VECTOR_DEFINITION); + module } -/// Checks if two characters are equal +macro_rules! monotonic { + ($iter:expr, $compare:expr) => {{ + let mut iter = $iter; + + let Some(mut last)= iter.next().transpose()? else { + stop!(ArityMismatch => "expected at least one argument"); + }; + + let comparator = $compare; + + for maybe_item in iter { + let item = maybe_item?; + + if comparator(&last, &item) { + last = item; + } else { + return Ok(SteelVal::BoolV(false)); + } + } + + Ok(SteelVal::BoolV(true)) + }}; +} + +/// Checks if all characters are equal. +/// +/// Requires that all inputs are characters, and will otherwise raise an error. /// -/// Requires that the two inputs are both characters, and will otherwise -/// raise an error. +/// (char=? char1 char2 ...) -> bool? +/// +/// * char1 : char? +/// * char2 : char? #[function(name = "char=?", constant = true)] -pub fn char_equals(left: char, right: char) -> bool { - left == right +pub fn char_equals(rest: RestArgsIter) -> Result { + monotonic!(rest, |ch1: &_, ch2: &_| ch1 == ch2) } fn number_to_string_impl(value: &SteelVal, radix: Option) -> Result { @@ -90,7 +123,7 @@ fn number_to_string_impl(value: &SteelVal, radix: Option) -> Result) -> Result { let radix = rest.next(); @@ -183,73 +216,110 @@ pub fn string_constructor(rest: RestArgsIter<'_, char>) -> Result { rest.collect::>().map(|x| x.into()) } -/// Compares two strings lexicographically (as in "less-than-or-equal"). -#[function(name = "string<=?", constant = true)] -pub fn string_less_than_equal_to(left: &SteelString, right: &SteelString) -> bool { - left <= right -} - -/// Compares two strings lexicographically (as in "less-than-or-equal"), -// in a case insensitive fashion. -#[function(name = "string-ci<=?", constant = true)] -pub fn string_ci_less_than_equal_to(left: &SteelString, right: &SteelString) -> bool { - left.to_lowercase() <= right.to_lowercase() -} - -/// Compares two strings lexicographically (as in "less-than"). -#[function(name = "string bool { - left < right -} - -/// Compares two strings lexicographically (as in "less-than"), -/// in a case insensitive fashion. -#[function(name = "string-ci bool { - left.to_lowercase() < right.to_lowercase() -} - -/// Compares two strings lexicographically (as in "greater-than-or-equal"). -#[function(name = "string>=?", constant = true)] -pub fn string_greater_than_equal_to(left: &SteelString, right: &SteelString) -> bool { - left >= right -} - -/// Compares two strings lexicographically (as in "greater-than-or-equal"), -/// in a case insensitive fashion. -#[function(name = "string-ci>=?", constant = true)] -pub fn string_ci_greater_than_equal_to(left: &SteelString, right: &SteelString) -> bool { - left.to_lowercase() >= right.to_lowercase() -} - -/// Compares two strings lexicographically (as in "greater-than"). -#[function(name = "string>?", constant = true)] -pub fn string_greater_than(left: &SteelString, right: &SteelString) -> bool { - left > right -} - -/// Compares two strings lexicographically (as in "greater-than"), -/// in a case-insensitive fashion. -#[function(name = "string-ci>?", constant = true)] -pub fn string_ci_greater_than(left: &SteelString, right: &SteelString) -> bool { - left.to_lowercase() > right.to_lowercase() +// TODO: avoid allocation in `-ci` variants + +macro_rules! impl_str_comparison { + (-ci, $name:ident, $ext_name:literal, $mode:literal, $op:expr) => { + #[doc = concat!("Compares strings lexicographically (as in\"", $mode, "\"),")] + #[doc = "in a case insensitive fashion."] + #[doc = ""] + #[doc = concat!("(", $ext_name, " s1 s2 ... ) -> bool?")] + #[doc = "* s1 : string?"] + #[doc = "* s2 : string?"] + #[function(name = $ext_name, constant = true)] + pub fn $name(rest: RestArgsIter<&SteelString>) -> Result { + monotonic!(rest.map(|val| val.map(|s| s.as_str().to_lowercase())), $op) + } + }; + ($name:ident, $ext_name:literal, $mode:literal, $op:expr) => { + #[doc = concat!("Compares strings lexicographically (as in\"", $mode, "\").")] + #[doc = ""] + #[doc = concat!("(", $ext_name, " s1 s2 ... ) -> bool?")] + #[doc = "* s1 : string?"] + #[doc = "* s2 : string?"] + #[function(name = $ext_name, constant = true)] + pub fn $name(rest: RestArgsIter<&SteelString>) -> Result { + monotonic!(rest, $op) + } + }; } -/// Compares two strings for equality. +impl_str_comparison!( + string_less_than, + "string?", + "greater-than", + |s1: &_, s2: &_| s1 > s2 +); +impl_str_comparison!( + string_greater_than_equal_to, + "string>=?", + "greater-than-or-equal", + |s1: &_, s2: &_| s1 >= s2 +); +impl_str_comparison!( + -ci, + string_ci_less_than, + "string-ci?", + "greater-than", + |s1: &_, s2: &_| s1 > s2 +); +impl_str_comparison!( + -ci, + string_ci_greater_than_equal_to, + "string-ci>=?", + "greater-than-or-equal", + |s1: &_, s2: &_| s1 >= s2 +); + +/// Compares strings for equality. +/// +/// (string=? string1 string2 ...) -> bool? +/// +/// * string1 : string? +/// * string2 : string? #[function(name = "string=?", constant = true)] -pub fn string_equals(left: &SteelString, right: &SteelString) -> bool { - left == right +pub fn string_equals(rest: RestArgsIter<&SteelString>) -> Result { + monotonic!(rest, |s1: &_, s2: &_| s1 == s2) } -/// Compares two strings for equality, in a case insensitive fashion. +/// Compares strings for equality, in a case insensitive fashion. #[function(name = "string-ci=?", constant = true)] -pub fn string_ci_equals(left: &SteelString, right: &SteelString) -> bool { - left.to_lowercase() == right.to_lowercase() +pub fn string_ci_equals(rest: RestArgsIter<&SteelString>) -> Result { + monotonic!( + rest.map(|val| val.map(|s| s.as_str().to_lowercase())), + |s1: &_, s2: &_| s1 == s2 + ) } /// Extracts the nth character out of a given string. /// -/// (string-ref str n) +/// (string-ref str n) -> char? /// /// * str : string? /// * n : int? @@ -287,48 +357,15 @@ pub fn substring( i: usize, mut rest: RestArgsIter<'_, isize>, ) -> Result { - use std::iter::once; - - if i > value.len() { - stop!(Generic => "substring: index out of bounds: left bound: {}, string length: {}", i, value.len()); - } - - let j = rest.next().transpose()?.map(|j| j as usize); + let j = rest.next().transpose()?; if rest.next().is_some() { stop!(ArityMismatch => "substring expects 1 or 2 arguments"); } - if let Some(j) = j { - if i > j { - stop!(Generic => "substring: left bound must be less than or equal to the right bound: left: {}, right: {}", i, j); - } - } - - if value.is_empty() { - return Ok(SteelVal::StringV("".into())); - } - - let mut char_offsets = value - .char_indices() - .map(|(offset, _)| offset) - .chain(once(value.len())); - - let Some(start) = char_offsets.nth(i) else { - stop!(Generic => "substring: index out of bounds: left bound: {}", i); - }; - - let Some(j) = j else { - return Ok(SteelVal::StringV(value[start..].into())); - }; - - let mut char_offsets = once(start).chain(char_offsets); + let range = bounds(value.as_str(), Some(i as isize), j, "substring")?; - let Some(end) = char_offsets.nth(j - i) else { - stop!(Generic => "substring: index out of bounds: right bound: {}", j); - }; - - Ok(SteelVal::StringV(value[start..end].into())) + Ok(SteelVal::StringV(value[range].into())) } /// Creates a string of a given length, filled with an optional character @@ -451,7 +488,11 @@ pub fn string_to_int(value: &SteelString) -> Result { /// Converts a string into a list of characters. /// -/// (string->list string?) -> (listof char?) +/// (string->list s [start] [end]) -> (listof char?) +/// +/// * s : string? +/// * start : int? = 0 +/// * end : int? /// /// # Examples /// @@ -459,12 +500,21 @@ pub fn string_to_int(value: &SteelString) -> Result { /// > (string->list "hello") ;; => '(#\h #\e #\l #\l #\o) /// ``` #[function(name = "string->list")] -pub fn string_to_list(value: &SteelString) -> SteelVal { - value +pub fn string_to_list(value: &SteelString, mut rest: RestArgsIter) -> Result { + let i = rest.next().transpose()?; + let j = rest.next().transpose()?; + + if rest.next().is_some() { + stop!(ArityMismatch => "string->list expects up to 3 arguments"); + } + + let range = bounds(value.as_str(), i, j, "string->list")?; + + Ok(value[range] .chars() .map(SteelVal::CharV) .collect::>() - .into() + .into()) } /// Creates a new uppercased version of the input string @@ -701,6 +751,200 @@ pub fn string_append(mut rest: RestArgsIter<'_, &SteelString>) -> Result { + #[doc = concat!("Compares characters according to their codepoints, in a \"", $mode, "\" fashion.")] + #[doc = ""] + #[doc = concat!("(", $ext_name, " char1 char2 ... ) -> bool?")] + #[doc = "* char1 : char?"] + #[doc = "* char2 : char?"] + #[function(name = $ext_name, constant = true)] + pub fn $name(rest: RestArgsIter<&char>) -> Result { + monotonic!(rest, $op) + } + }; +} + +impl_char_comparison!( + char_less_than, + "char?", + "greater-than", + |ch1: &_, ch2: &_| ch1 > ch2 +); +impl_char_comparison!( + char_greater_than_equal_to, + "char>=?", + "greater-than-or-equal", + |ch1: &_, ch2: &_| ch1 >= ch2 +); + +/// Returns the Unicode codepoint of a given character. +/// +/// (char->integer char?) -> integer? +#[function(name = "char->integer")] +pub fn char_to_integer(ch: char) -> u32 { + ch as u32 +} + +/// Returns the character corresponding to a given Unicode codepoint. +/// +/// (integer->char integer?) -> char? +#[function(name = "integer->char")] +pub fn integer_to_char(int: u32) -> Result { + let Some(ch) = char::from_u32(int) else { + stop!(ConversionError => "integer {} is out of range for a character", int); + }; + + Ok(ch.into()) +} + +/// Encodes a string as UTF-8 into a bytevector. +/// +/// (string->bytes string?) -> bytes? +/// +/// # Examples +/// ```scheme +/// (string->bytes "Apple") ;; => (bytes 65 112 112 108 101) +/// ``` +#[function(name = "string->bytes", alias = "string->utf8")] +pub fn string_to_bytes(value: &SteelString, mut rest: RestArgsIter) -> Result { + let start = rest.next().transpose()?; + let end = rest.next().transpose()?; + + if rest.next().is_some() { + stop!(ArityMismatch => "string->bytes expects up to 3 parameters"); + } + + let range = bounds(value.as_str(), start, end, "string->bytes")?; + + let bytes = value.as_bytes()[range].to_owned(); + + Ok(SteelVal::ByteVector(SteelByteVector::new(bytes))) +} + +/// Returns a vector containing the characters of a given string +/// +/// (string->vector string?) -> vector? +/// +/// # Examples +/// ```scheme +/// (string->vector "hello") ;; => '#(#\h #\e #\l #\l #\o) +/// ``` +#[function(name = "string->vector")] +pub fn string_to_vector(value: &SteelString, mut rest: RestArgsIter) -> Result { + let start = rest.next().transpose()?; + let end = rest.next().transpose()?; + + if rest.next().is_some() { + stop!(ArityMismatch => "string->vector expects up to 3 parameters"); + } + + let range = bounds(value.as_str(), start, end, "string->vector")?; + + let chars: im_rc::Vector<_> = value[range].chars().map(SteelVal::CharV).collect(); + + Ok(SteelVal::VectorV(Gc::new(chars).into())) +} + +fn bounds( + s: &str, + i: Option, + j: Option, + name: &str, +) -> Result> { + use std::iter::once; + + let i = i.unwrap_or(0); + + if i < 0 { + stop!(ContractViolation => "{}: bounds must be non-negative: left: {}", name, i); + } + + let i = i as usize; + + if i > s.len() { + stop!(Generic => "{}: index out of bounds: left bound: {}, string length: {}", name, i, s.len()); + } + + if let Some(j) = j { + if j < 0 { + stop!(ContractViolation => "{}: bounds must be non-negative: right: {}", name, j); + } + + if i > (j as usize) { + stop!(Generic => "{}: left bound must be less than or equal to the right bound: left: {}, right: {}", name, i, j); + } + } + + let j = j.map(|j| j as usize); + + let mut char_offsets = s + .char_indices() + .map(|(offset, _)| offset) + .chain(once(s.len())); + + let Some(start) = char_offsets.nth(i) else { + stop!(Generic => "{}: index out of bounds: left bound: {}", name, i); + }; + + let Some(j) = j else { + return Ok(start..s.len()); + }; + + let mut char_offsets = once(start).chain(char_offsets); + + let Some(end) = char_offsets.nth(j - i) else { + stop!(Generic => "{}: index out of bounds: right bound: {}", name, j); + }; + + Ok(start..end) +} + +/// Returns the upper case version of a character, if defined by Unicode, +/// or the same character otherwise. +#[function(name = "char-upcase")] +fn char_upcase(c: char) -> char { + c.to_uppercase().next().unwrap() +} + +/// Returns the lower case version of a character, if defined by Unicode, +/// or the same character otherwise. +#[function(name = "char-downcase")] +fn char_downcase(c: char) -> char { + c.to_lowercase().next().unwrap() +} + +/// Returns `#t` if the character is a whitespace character. +#[function(name = "char-whitespace?")] +fn char_is_whitespace(c: char) -> bool { + c.is_whitespace() +} + +/// Returns `#t` if the character is a decimal digit. +#[function(name = "char-digit?")] +fn char_is_digit(c: char) -> bool { + c.is_digit(10) +} + +/// Attemps to convert the character into a decimal digit, +/// and returns `#f` on failure. +#[function(name = "char->number")] +fn char_to_number(c: char) -> Option { + c.to_digit(10) +} + #[cfg(test)] mod string_operation_tests { use super::*; @@ -758,7 +1002,6 @@ mod string_operation_tests { ("trim", trim_arity_too_many, steel_trim), ("trim-start", trim_start_arity_too_many, steel_trim_start), ("trim-end", trim_end_arity_too_many, steel_trim_end), - ("string->list", string_to_list_arity_too_many, steel_string_to_list), ("split-whitespace", split_whitespace_arity_too_many, steel_split_whitespace), } @@ -779,6 +1022,8 @@ mod string_operation_tests { ("trim-start", trim_start_arity_takes_string, steel_trim_start), ("trim-end", trim_end_arity_takes_string, steel_trim_end), ("string->list", string_to_list_takes_string, steel_string_to_list), + ("string->bytes", string_to_bytes_takes_string, steel_string_to_bytes), + ("string->vector", string_to_vector_takes_string, steel_string_to_vector), ("split-whitespace", split_whitespace_arity_takes_string, steel_split_whitespace) } diff --git a/crates/steel-core/src/primitives/time.rs b/crates/steel-core/src/primitives/time.rs index 0fca81031..9ce25cf85 100644 --- a/crates/steel-core/src/primitives/time.rs +++ b/crates/steel-core/src/primitives/time.rs @@ -9,7 +9,7 @@ use steel_derive::function; use crate::steel_vm::builtin::BuiltInModule; use crate::steel_vm::register_fn::RegisterFn; -pub(crate) const TIME_MODULE_DOC: MarkdownDoc<'static> = MarkdownDoc( +pub(crate) const TIME_MODULE_DOC: MarkdownDoc<'static> = MarkdownDoc::from_str( r#" Contains direct wrappers around the Rust `std::time::Instant` and `std::time::Duration` modules. For example, to measure the time something takes: diff --git a/crates/steel-core/src/scheme/modules/parameters.scm b/crates/steel-core/src/scheme/modules/parameters.scm index fc4687deb..6e30ce73e 100644 --- a/crates/steel-core/src/scheme/modules/parameters.scm +++ b/crates/steel-core/src/scheme/modules/parameters.scm @@ -44,33 +44,35 @@ (provide current-input-port current-output-port + current-error-port simple-display simple-displayln newline write-char - write) + write + write-string) (define current-input-port (make-parameter (#%default-input-port))) (define current-output-port (make-parameter (#%default-output-port))) +(define current-error-port (make-parameter (#%default-error-port))) (define (simple-display x) - (raw-write-string (current-output-port) x)) + (#%raw-write-string x (current-output-port))) + +(define write-string #%raw-write-string) (define newline (case-lambda - [() (raw-write-char (current-output-port) #\newline)] - [(port) (raw-write-char port #\newline)])) + [() (#%raw-write-char #\newline (current-output-port))] + [(port) (#%raw-write-char #\newline port)])) (define (simple-displayln x) (simple-display x) (newline)) -;; TODO: Swap argument order of primitive -(define (write-char char port) - (raw-write-char port char)) +(define write-char #%raw-write-char) -(define (write obj port) - (raw-write port obj)) +(define write #%raw-write) ;;;;;;;;;;;;;;;;;;;;; Port functions ;;;;;;;;;;;;;;;;;;;;; diff --git a/crates/steel-core/src/steel_vm/builtin.rs b/crates/steel-core/src/steel_vm/builtin.rs index f8cde9a7d..970422acc 100644 --- a/crates/steel-core/src/steel_vm/builtin.rs +++ b/crates/steel-core/src/steel_vm/builtin.rs @@ -158,6 +158,7 @@ pub enum BuiltInFunctionTypePointer { pub struct NativeFunctionDefinition { pub name: &'static str, + pub aliases: &'static [&'static str], pub func: BuiltInFunctionType, pub arity: Arity, pub doc: Option>, @@ -490,20 +491,27 @@ impl BuiltInModule { if let Some(doc) = definition.doc { self.register_doc(definition.name, doc); - } - match definition.func { - BuiltInFunctionType::Reference(value) => { - self.register_value(definition.name, SteelVal::FuncV(value)); - } - BuiltInFunctionType::Mutable(value) => { - self.register_value(definition.name, SteelVal::MutFunc(value)); - } - BuiltInFunctionType::Context(value) => { - self.register_value(definition.name, SteelVal::BuiltIn(value)); + for alias in definition.aliases { + self.register_doc( + *alias, + MarkdownDoc(format!("Alias of `{}`.", definition.name).into()), + ); } } + let steel_val = match definition.func { + BuiltInFunctionType::Reference(value) => SteelVal::FuncV(value), + BuiltInFunctionType::Mutable(value) => SteelVal::MutFunc(value), + BuiltInFunctionType::Context(value) => SteelVal::BuiltIn(value), + }; + + let names = std::iter::once(definition.name).chain(definition.aliases.into_iter().cloned()); + + for name in names { + self.register_value(name, steel_val.clone()); + } + self } @@ -713,12 +721,18 @@ pub struct ValueDoc<'a> { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct MarkdownDoc<'a>(pub &'a str); +pub struct MarkdownDoc<'a>(pub Cow<'a, str>); + +impl<'a> MarkdownDoc<'a> { + pub const fn from_str(s: &'a str) -> Self { + Self(Cow::Borrowed(s)) + } +} impl<'a> std::fmt::Display for MarkdownDoc<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[cfg(feature = "markdown")] - return write!(f, "{}", termimad::text(self.0)); + return write!(f, "{}", termimad::text(&self.0)); #[cfg(not(feature = "markdown"))] return write!(f, "{}", self.0); diff --git a/crates/steel-core/src/steel_vm/const_evaluation.rs b/crates/steel-core/src/steel_vm/const_evaluation.rs index cd5665a70..f75adf622 100644 --- a/crates/steel-core/src/steel_vm/const_evaluation.rs +++ b/crates/steel-core/src/steel_vm/const_evaluation.rs @@ -593,7 +593,7 @@ impl<'a> ConsumingVisitor for ConstantEvaluator<'a> { // Check if its a function application, and go for it fn visit_list(&mut self, l: crate::parser::ast::List) -> Self::Output { if l.args.is_empty() { - stop!(BadSyntax => "empty function application"; get_span(&ExprKind::List(l))); + stop!(BadSyntax => "empty function application"; l.location.unwrap_or_else(|| get_span(&ExprKind::List(l)))); } if l.args.len() == 1 { diff --git a/crates/steel-core/src/steel_vm/primitives.rs b/crates/steel-core/src/steel_vm/primitives.rs index 9b5aec31f..58d42ecfb 100644 --- a/crates/steel-core/src/steel_vm/primitives.rs +++ b/crates/steel-core/src/steel_vm/primitives.rs @@ -18,6 +18,7 @@ use crate::{ hashsets::hashset_module, lists::{list_module, UnRecoverableResult}, numbers, port_module, + ports::EOF_OBJECTP_DEFINITION, process::process_module, random::random_module, string_module, @@ -741,6 +742,11 @@ fn structp(value: &SteelVal) -> bool { } } +#[steel_derive::function(name = "port?", constant = true)] +fn portp(value: &SteelVal) -> bool { + matches!(value, SteelVal::PortV(..)) +} + #[steel_derive::function(name = "#%private-struct?", constant = true)] fn private_structp(value: &SteelVal) -> bool { matches!(value, SteelVal::CustomStruct(_)) @@ -799,6 +805,8 @@ fn identity_module() -> BuiltInModule { .register_native_fn_definition(BOOLP_DEFINITION) .register_native_fn_definition(VOIDP_DEFINITION) .register_native_fn_definition(STRUCTP_DEFINITION) + .register_native_fn_definition(PORTP_DEFINITION) + .register_native_fn_definition(EOF_OBJECTP_DEFINITION) .register_native_fn_definition(PRIVATE_STRUCTP_DEFINITION) .register_value("mutable-vector?", gen_pred!(MutableVector)) .register_value("char?", gen_pred!(CharV)) @@ -1009,8 +1017,9 @@ fn sandboxed_io_module() -> BuiltInModule { // .register_value("read-to-string", IoFunctions::read_to_string()); module } -pub const VOID_DOC: MarkdownDoc = - MarkdownDoc("The void value, returned by many forms with side effects, such as `define`."); +pub const VOID_DOC: MarkdownDoc = MarkdownDoc::from_str( + "The void value, returned by many forms with side effects, such as `define`.", +); /// Miscellaneous constants #[steel_derive::define_module(name = "steel/constants")] @@ -1290,9 +1299,7 @@ impl Reader { Ok(SteelVal::Void) } } else { - Ok(SteelVal::SymbolV( - crate::primitives::ports::EOF_OBJECT.with(|x| x.clone()), - )) + Ok(crate::primitives::ports::eof()) } } } diff --git a/crates/steel-core/src/steel_vm/vm.rs b/crates/steel-core/src/steel_vm/vm.rs index c8edfea85..02bd56e78 100644 --- a/crates/steel-core/src/steel_vm/vm.rs +++ b/crates/steel-core/src/steel_vm/vm.rs @@ -4005,7 +4005,7 @@ pub fn call_cc(ctx: &mut VmCore, args: &[SteelVal]) -> Option> Some(Ok(SteelVal::ContinuationFunction(continuation))) } -pub(crate) const APPLY_DOC: MarkdownDoc<'static> = MarkdownDoc( +pub(crate) const APPLY_DOC: MarkdownDoc<'static> = MarkdownDoc::from_str( r#" Applies the given `function` with arguments as the contents of the `list`. diff --git a/crates/steel-core/src/values/json_vals.rs b/crates/steel-core/src/values/json_vals.rs index 6c203894f..0ff81fa68 100644 --- a/crates/steel-core/src/values/json_vals.rs +++ b/crates/steel-core/src/values/json_vals.rs @@ -12,7 +12,7 @@ use steel_derive::function; /// Deserializes a JSON string into a Steel value. /// -/// (string->jsexpr json) -> any/c? +/// (string->jsexpr json) -> any/c /// /// * json : string? /// @@ -33,7 +33,7 @@ pub fn string_to_jsexpr(value: &SteelString) -> Result { /// Serializes a Steel value into a string. /// -/// (value->jsexpr-string any/c?) -> string? +/// (value->jsexpr-string any/c) -> string? /// /// # Examples /// ```scheme diff --git a/crates/steel-core/src/values/port.rs b/crates/steel-core/src/values/port.rs index dd2f555d8..9fb34a942 100644 --- a/crates/steel-core/src/values/port.rs +++ b/crates/steel-core/src/values/port.rs @@ -3,6 +3,7 @@ use std::fs::OpenOptions; use std::io; use std::io::prelude::*; use std::io::Cursor; +use std::io::Stderr; use std::io::{BufReader, BufWriter, Stdin, Stdout}; use std::process::ChildStdin; use std::process::ChildStdout; @@ -13,9 +14,7 @@ use std::sync::Mutex; // use utils::chars::Chars; // use utils::{new_rc_ref_cell, RcRefCell}; -use crate::rerrs; use crate::rvals::Result; -use crate::SteelErr; // use crate::rvals::{new_rc_ref_cell, RcRefSteelVal}; @@ -49,10 +48,11 @@ pub enum SteelPortRepr { FileOutput(String, BufWriter), StdInput(Stdin), StdOutput(Stdout), + StdError(Stderr), ChildStdOutput(BufReader), ChildStdInput(BufWriter), - StringInput(BufReader>>), - StringOutput(BufWriter>), + StringInput(Cursor>), + StringOutput(Vec), DynWriter(Arc>), // DynReader(Box), Closed, @@ -69,6 +69,7 @@ impl std::fmt::Debug for SteelPortRepr { } SteelPortRepr::StdInput(s) => f.debug_tuple("StdInput").field(s).finish(), SteelPortRepr::StdOutput(s) => f.debug_tuple("StdOutput").field(s).finish(), + SteelPortRepr::StdError(s) => f.debug_tuple("StdError").field(s).finish(), SteelPortRepr::ChildStdOutput(s) => f.debug_tuple("ChildStdOutput").field(s).finish(), SteelPortRepr::ChildStdInput(s) => f.debug_tuple("ChildStdInput").field(s).finish(), SteelPortRepr::StringInput(s) => f.debug_tuple("StringInput").field(s).finish(), @@ -82,6 +83,7 @@ impl std::fmt::Debug for SteelPortRepr { pub enum SendablePort { StdInput(Stdin), StdOutput(Stdout), + StdError(Stderr), BoxDynWriter(Arc>), Closed, } @@ -91,6 +93,7 @@ impl SendablePort { match value { SteelPortRepr::StdInput(_) => Ok(SendablePort::StdInput(io::stdin())), SteelPortRepr::StdOutput(_) => Ok(SendablePort::StdOutput(io::stdout())), + SteelPortRepr::StdError(_) => Ok(SendablePort::StdError(io::stderr())), SteelPortRepr::Closed => Ok(SendablePort::Closed), _ => { stop!(Generic => "Unable to send port across threads: {:?}", value) @@ -112,6 +115,9 @@ impl SteelPort { SendablePort::StdOutput(s) => SteelPort { port: new_rc_ref_cell(SteelPortRepr::StdOutput(s)), }, + SendablePort::StdError(s) => SteelPort { + port: new_rc_ref_cell(SteelPortRepr::StdError(s)), + }, SendablePort::Closed => SteelPort { port: new_rc_ref_cell(SteelPortRepr::Closed), }, @@ -152,6 +158,7 @@ impl SteelPortRepr { match self { SteelPortRepr::FileInput(_, br) => port_read_str_fn!(br, read_line), SteelPortRepr::StdInput(br) => port_read_str_fn!(br, read_line), + SteelPortRepr::StringInput(s) => port_read_str_fn!(s, read_line), SteelPortRepr::ChildStdOutput(br) => { // let buf_reader = BufReader::new(br.borrow_mut().as_mut()); @@ -189,96 +196,122 @@ impl SteelPortRepr { } } - pub fn read_char(&mut self) -> Result<(usize, char)> { - // FIXME: this only reads 1 u8 and casts it to char - macro_rules! port_read_chr( - ($br: ident) => {{ - let mut chr = [0; 1]; - $br.read_exact(&mut chr)?; - Ok((1, chr[0] as char)) - }}; - ); + pub fn read_char(&mut self) -> Result> { + let mut buf = [0; 4]; - match self { - SteelPortRepr::FileInput(_, br) => port_read_chr!(br), - SteelPortRepr::StdInput(br) => port_read_chr!(br), - _x => stop!(Generic => "read-char"), + for i in 0..4 { + let result = self.read_byte()?; + + let b = match result { + Some(b) => b, + None => { + if i == 0 { + return Ok(None); + } else { + stop!(ConversionError => "unable to decode character, found {:?}", &buf[0..=i]); + } + } + }; + + buf[i] = b; + + match std::str::from_utf8(&buf[0..=i]) { + Ok(s) => return Ok(s.chars().next()), + Err(err) if err.error_len().is_some() => { + stop!(ConversionError => "unable to decode character, found {:?}", &buf[0..=i]); + } + _ => {} + } } + + stop!(ConversionError => "unable to decode character, found {:?}", buf); } - pub fn write_char(&mut self, c: char) -> Result<()> { - macro_rules! write_string( - ($br: ident) => {{ - write!($br, "{}", c)?; - $br.flush()?; - }}; - ); + pub fn read_byte(&mut self) -> Result> { + let mut byte = [0]; - match self { - SteelPortRepr::FileOutput(_, br) => write_string!(br), - SteelPortRepr::StringOutput(br) => write_string!(br), - SteelPortRepr::StdOutput(out) => { - let mut br = out.lock(); - write!(br, "{}", c)?; - br.flush()?; - } - SteelPortRepr::DynWriter(o) => { - let mut br = o.lock().unwrap(); - write!(br, "{}", c)?; - br.flush()?; - } - _x => stop!(Generic => "write-car"), + let result = match self { + SteelPortRepr::FileInput(_, reader) => reader.read_exact(&mut byte), + SteelPortRepr::StdInput(stdin) => stdin.read_exact(&mut byte), + SteelPortRepr::ChildStdOutput(output) => output.read_exact(&mut byte), + SteelPortRepr::StringInput(reader) => reader.read_exact(&mut byte), + SteelPortRepr::FileOutput(_, _) + | SteelPortRepr::StdOutput(_) + | SteelPortRepr::StdError(_) + | SteelPortRepr::ChildStdInput(_) + | SteelPortRepr::StringOutput(_) + | SteelPortRepr::DynWriter(_) => stop!(ContractViolation => "expected input-port?"), + SteelPortRepr::Closed => return Ok(None), }; - Ok(()) + if let Err(err) = result { + if err.kind() == io::ErrorKind::UnexpectedEof { + return Ok(None); + } + + return Err(err.into()); + } + + Ok(Some(byte[0])) } - pub fn write_string(&mut self, string: &str) -> Result<()> { - macro_rules! write_string( - ($br: ident) => {{ - write!($br, "{}", string)?; - $br.flush()?; - }}; - ); + pub fn peek_byte(&mut self) -> Result> { + let mut buf = [0]; - match self { - SteelPortRepr::FileOutput(_, br) => write_string!(br), - SteelPortRepr::StdOutput(out) => { - let mut br = out.lock(); - write!(br, "{}", string)?; - br.flush()?; - } - SteelPortRepr::DynWriter(o) => { - let mut br = o.lock().unwrap(); - write!(br, "{}", string)?; - br.flush()?; - } - SteelPortRepr::StringOutput(br) => write_string!(br), - _x => stop!(Generic => "write-string"), + let result = self.peek(&mut buf)?; + + if result == 0 { + Ok(None) + } else { + Ok(Some(buf[0])) + } + } + + // This would not work for `peek_char`, since a `BufRead` will not work for this purpose. + // We need a way to force-fill the internal buffer up to 4 bytes. `fill_buf()` only tries to + // fill _if_ the internal buffer is empty, and without any guarantees of returned size. + fn peek(&mut self, buf: &mut [u8]) -> Result { + let copy = |src: &[u8]| { + let len = src.len().min(buf.len()); + + buf.copy_from_slice(&src[0..len]); + + len }; + let result = match self { + SteelPortRepr::FileInput(_, reader) => reader.fill_buf().map(copy), + SteelPortRepr::StdInput(stdin) => { + let mut lock = stdin.lock(); + lock.fill_buf().map(copy) + } + SteelPortRepr::ChildStdOutput(output) => output.fill_buf().map(copy), + SteelPortRepr::StringInput(reader) => reader.fill_buf().map(copy), + SteelPortRepr::FileOutput(_, _) + | SteelPortRepr::StdOutput(_) + | SteelPortRepr::StdError(_) + | SteelPortRepr::ChildStdInput(_) + | SteelPortRepr::StringOutput(_) + | SteelPortRepr::DynWriter(_) => stop!(ContractViolation => "expected input-port?"), + SteelPortRepr::Closed => return Ok(0), + }?; + + Ok(result) + } + + pub fn write_char(&mut self, c: char) -> Result<()> { + let mut buf = [0; 4]; + + let s = c.encode_utf8(&mut buf); + + let _ = self.write(s.as_bytes())?; + Ok(()) } pub fn write_string_line(&mut self, string: &str) -> Result<()> { - macro_rules! write_string( - ($br: ident) => {{ - write!($br, "{}\n", string)?; - $br.flush()?; - }}; - ); - - match self { - SteelPortRepr::FileOutput(_, br) => write_string!(br), - SteelPortRepr::StdOutput(br) => write_string!(br), - SteelPortRepr::ChildStdInput(br) => write_string!(br), - SteelPortRepr::StringOutput(br) => write_string!(br), - SteelPortRepr::DynWriter(br) => { - let mut br = br.lock().unwrap(); - write_string!(br) - } - _x => stop!(Generic => "write-string"), - }; + let _ = self.write(string.as_bytes())?; + let _ = self.write(b"\n")?; Ok(()) } @@ -286,7 +319,10 @@ impl SteelPortRepr { pub fn is_input(&self) -> bool { matches!( self, - SteelPortRepr::FileInput(_, _) | SteelPortRepr::StdInput(_) + SteelPortRepr::FileInput(_, _) + | SteelPortRepr::StdInput(_) + | SteelPortRepr::ChildStdOutput(_) + | SteelPortRepr::StringInput(_) ) } @@ -295,42 +331,56 @@ impl SteelPortRepr { self, SteelPortRepr::FileOutput(_, _) | SteelPortRepr::StdOutput(_) + | SteelPortRepr::StdError(_) | SteelPortRepr::DynWriter(_) + | SteelPortRepr::ChildStdInput(_) + | SteelPortRepr::StringOutput(_) ) } - pub fn is_textual(&self) -> bool { - matches!( - self, - SteelPortRepr::FileInput(_, _) - | SteelPortRepr::FileOutput(_, _) - | SteelPortRepr::StdOutput(_) - | SteelPortRepr::StdInput(_) - ) - } + pub fn get_output(&self) -> Result>> { + let buf: &Vec = if let SteelPortRepr::StringOutput(s) = self { + s + } else { + return Ok(None); + }; - pub fn get_output_string(&mut self) -> Result { - if let SteelPortRepr::StringOutput(s) = self { - // Ensure that this is flushed - s.flush()?; + Ok(Some(buf.clone())) + } - String::from_utf8(s.get_ref().to_vec()) - .map_err(|err| SteelErr::new(rerrs::ErrorKind::Generic, err.to_string())) + pub fn close_output_port(&mut self) -> Result<()> { + if self.is_output() { + *self = SteelPortRepr::Closed; + Ok(()) } else { - stop!(TypeMismatch => "get-output-string expects an output port, found: {:?}", self); + stop!(TypeMismatch => "close-output-port expects an output port, found: {:?}", self) } } - pub fn close_output_port(&mut self) -> Result<()> { - match self { - SteelPortRepr::FileOutput(_, _) | SteelPortRepr::StdOutput(_) => { - *self = SteelPortRepr::Closed; - Ok(()) - } - _ => { - stop!(TypeMismatch => "close-output-port expects an output port, found: {:?}", self) - } - } + pub fn write(&mut self, buf: &[u8]) -> Result { + macro_rules! write_and_flush( + ($br: expr) => {{ + let result = $br.write(buf)?; + $br.flush()?; + result + }}; + ); + + let result = match self { + SteelPortRepr::FileOutput(_, writer) => write_and_flush![writer], + SteelPortRepr::StdOutput(writer) => write_and_flush![writer], + SteelPortRepr::StdError(writer) => write_and_flush![writer], + SteelPortRepr::ChildStdInput(writer) => write_and_flush![writer], + SteelPortRepr::StringOutput(writer) => write_and_flush![writer], + SteelPortRepr::DynWriter(writer) => write_and_flush![writer.lock().unwrap()], + SteelPortRepr::FileInput(_, _) + | SteelPortRepr::StdInput(_) + | SteelPortRepr::ChildStdOutput(_) + | SteelPortRepr::StringInput(_) => stop!(ContractViolation => "expected output-port?"), + SteelPortRepr::Closed => stop!(Io => "port is closed"), + }; + + Ok(result) } } @@ -363,15 +413,19 @@ impl SteelPort { pub fn new_input_port_string(string: String) -> SteelPort { SteelPort { - port: new_rc_ref_cell(SteelPortRepr::StringInput(BufReader::new(Cursor::new( - string.into_bytes(), - )))), + port: new_rc_ref_cell(SteelPortRepr::StringInput(Cursor::new(string.into_bytes()))), } } - pub fn new_output_port() -> SteelPort { + pub fn new_input_port_bytevector(vec: Vec) -> SteelPort { SteelPort { - port: new_rc_ref_cell(SteelPortRepr::StringOutput(BufWriter::new(Vec::new()))), + port: new_rc_ref_cell(SteelPortRepr::StringInput(Cursor::new(vec))), + } + } + + pub fn new_output_port_string() -> SteelPort { + SteelPort { + port: new_rc_ref_cell(SteelPortRepr::StringOutput(Vec::new())), } } @@ -391,19 +445,29 @@ impl SteelPort { self.port.borrow_mut().read_all_str() } - pub fn read_char(&self) -> Result<(usize, char)> { + pub fn read_char(&self) -> Result> { self.port.borrow_mut().read_char() } - pub fn write_char(&self, c: char) -> Result<()> { - self.port.borrow_mut().write_char(c) + pub fn read_byte(&self) -> Result> { + self.port.borrow_mut().read_byte() + } + + pub fn peek_byte(&self) -> Result> { + self.port.borrow_mut().peek_byte() } // // Write functions // - pub fn write_string(&self, string: &str) -> Result<()> { - self.port.borrow_mut().write_string(string) + pub fn write_char(&self, c: char) -> Result<()> { + self.port.borrow_mut().write_char(c) + } + + pub fn write(&self, buf: &[u8]) -> Result<()> { + let _ = self.port.borrow_mut().write(buf)?; + + Ok(()) } pub fn write_string_line(&self, string: &str) -> Result<()> { @@ -421,10 +485,6 @@ impl SteelPort { self.port.borrow().is_output() } - pub fn is_textual(&self) -> bool { - self.port.borrow().is_textual() - } - pub fn default_current_input_port() -> Self { SteelPort { port: new_rc_ref_cell(SteelPortRepr::StdInput(io::stdin())), @@ -446,8 +506,23 @@ impl SteelPort { } } - pub fn get_output_string(&self) -> Result { - self.port.borrow_mut().get_output_string() + pub fn default_current_error_port() -> Self { + if cfg!(test) { + // Write out to thread safe port + SteelPort { + port: new_rc_ref_cell(SteelPortRepr::DynWriter(Arc::new(Mutex::new( + BufWriter::new(Vec::new()), + )))), + } + } else { + SteelPort { + port: new_rc_ref_cell(SteelPortRepr::StdError(io::stderr())), + } + } + } + + pub fn get_output(&self) -> Result>> { + self.port.borrow().get_output() } pub fn close_output_port(&self) -> Result<()> { diff --git a/crates/steel-core/src/values/structs.rs b/crates/steel-core/src/values/structs.rs index 90f30b970..c9ab6f7de 100644 --- a/crates/steel-core/src/values/structs.rs +++ b/crates/steel-core/src/values/structs.rs @@ -488,30 +488,16 @@ pub fn make_struct_type(args: &[SteelVal]) -> Result { // Convert the string into an Arc'd string - this now makes the generated functions // thread safe. - let name: InternedString = if let SteelVal::SymbolV(s) = &args[0] { - Ok::<_, SteelErr>(s) - } else { + let SteelVal::SymbolV(name) = &args[0] else { stop!(TypeMismatch => format!("make-struct-type expected a symbol for the name, found: {}", &args[0])); - }?.as_str().into(); + }; - let field_count = if let SteelVal::IntV(i) = &args[1] { - Ok::<_, SteelErr>(i) - } else { + let SteelVal::IntV(field_count) = &args[1] else { stop!(TypeMismatch => format!("make-struct-type expected an integer for the field count, found: {}", &args[0])); - }?; - - // Make a slot in the VTable for this struct - let struct_type_descriptor = VTable::new_entry(name, None); - - // Build out the constructor and the predicate - let struct_constructor = - UserDefinedStruct::constructor(name, *field_count as usize, struct_type_descriptor); - let struct_predicate = UserDefinedStruct::predicate(struct_type_descriptor); - - let getter_prototype = UserDefinedStruct::getter_prototype(struct_type_descriptor); + }; - // We do not have the properties yet. Should probably intern the - // let struct_type_id = new_type_id(name, address_or_name) + let (struct_type_descriptor, struct_constructor, struct_predicate, getter_prototype) = + make_struct_type_inner(name.as_str(), *field_count as usize); Ok(SteelVal::ListV( vec![ @@ -526,6 +512,38 @@ pub fn make_struct_type(args: &[SteelVal]) -> Result { )) } +pub fn make_struct_singleton(name: &str) -> (SteelVal, StructTypeDescriptor) { + let (descriptor, _, _, _) = make_struct_type_inner(name, 0); + + let instance = UserDefinedStruct::new(descriptor, &[]); + + (SteelVal::CustomStruct(Gc::new(instance)), descriptor) +} + +fn make_struct_type_inner( + name: &str, + field_count: usize, +) -> (StructTypeDescriptor, SteelVal, SteelVal, SteelVal) { + let name = InternedString::from(name); + + // Make a slot in the VTable for this struct + let struct_type_descriptor = VTable::new_entry(name, None); + + // Build out the constructor and the predicate + let struct_constructor = + UserDefinedStruct::constructor(name, field_count, struct_type_descriptor); + let struct_predicate = UserDefinedStruct::predicate(struct_type_descriptor); + + let getter_prototype = UserDefinedStruct::getter_prototype(struct_type_descriptor); + + ( + struct_type_descriptor, + struct_constructor, + struct_predicate, + getter_prototype, + ) +} + // Implement internal thing here? struct SteelTrait { name: InternedString, diff --git a/crates/steel-derive/src/lib.rs b/crates/steel-derive/src/lib.rs index 157f8804a..22a039807 100644 --- a/crates/steel-derive/src/lib.rs +++ b/crates/steel-derive/src/lib.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use syn::{ - punctuated::Punctuated, Data, DeriveInput, Expr, ExprLit, FnArg, Ident, ItemFn, Lit, Meta, - ReturnType, Signature, Type, TypeReference, + punctuated::Punctuated, spanned::Spanned, Data, DeriveInput, Expr, ExprGroup, ExprLit, FnArg, + Ident, ItemFn, Lit, LitStr, Meta, ReturnType, Signature, Type, TypeReference, }; #[proc_macro_derive(Steel)] @@ -39,18 +39,27 @@ fn parse_key_value_pairs(args: &Punctuated) -> HashMap { - map.insert(key, s.value()); - } - Expr::Lit(ExprLit { - lit: Lit::Bool(b), .. - }) => { - map.insert(key, b.value().to_string()); + let mut value = &n.value; + + loop { + match value { + Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) => { + map.insert(key, s.value()); + break; + } + Expr::Lit(ExprLit { + lit: Lit::Bool(b), .. + }) => { + map.insert(key, b.value().to_string()); + break; + } + Expr::Group(ExprGroup { expr, .. }) => { + value = &**expr; + } + _ => break, } - _ => {} } } } @@ -58,7 +67,9 @@ fn parse_key_value_pairs(args: &Punctuated) -> HashMap Option { +fn parse_doc_comment(input: ItemFn) -> Option { + let span = input.span(); + let maybe_str_literals = input .attrs .into_iter() @@ -71,35 +82,60 @@ fn parse_doc_comment(input: ItemFn) -> Option { .map(|expr| match expr { Expr::Lit(ExprLit { lit: Lit::Str(s), .. - }) => Ok(s.value()), + }) => Ok(s), e => Err(e), }) - .collect::, _>>(); - - let literals = match maybe_str_literals { - Ok(lits) => lits, - Err(_) => { - return None; - // Error::new(expr.span(), "Doc comment is not a string literal") - // .into_compile_error() - // .into() - } - }; + .collect::>(); - if literals.is_empty() { + if maybe_str_literals.is_empty() { return None; - // Error::new(ident.span(), "No doc comment found on this type") - // .into_compile_error() - // .into(); } - let trimmed: Vec<_> = literals + if let Some(literals) = maybe_str_literals .iter() - .flat_map(|lit| lit.split('\n').collect::>()) - .map(|line| line.trim().to_string()) - .collect(); + .map(|item| item.as_ref().ok()) + .collect::>>() + { + let trimmed: Vec<_> = literals + .iter() + .flat_map(|lit| { + lit.value() + .split('\n') + .map(|s| s.to_string()) + .collect::>() + }) + .map(|line| line.trim().to_string()) + .collect(); + + let doc = trimmed.join("\n"); - Some(trimmed.join("\n")) + return Some(quote! { #doc }); + } + + let mut args = vec![]; + + for (i, item) in maybe_str_literals.into_iter().enumerate() { + if i > 0 { + args.push(Expr::Lit(ExprLit { + attrs: vec![], + lit: Lit::Str(LitStr::new("\n", span)), + })); + } + + let expr = match item { + Ok(lit) => Expr::Lit(ExprLit { + attrs: vec![], + lit: Lit::Str(lit), + }), + Err(expr) => expr, + }; + + args.push(expr); + } + + return Some(quote! { + concat![#(#args),*] + }); } #[proc_macro_attribute] @@ -128,7 +164,7 @@ pub fn define_module( let mut module = #function_name(); - module.register_doc(#value, crate::steel_vm::builtin::MarkdownDoc(#doc_comments)); + module.register_doc(#value, crate::steel_vm::builtin::MarkdownDoc(#doc_comments.into())); module } @@ -184,9 +220,10 @@ pub fn native( quote! { pub const #doc_name: crate::steel_vm::builtin::NativeFunctionDefinition = crate::steel_vm::builtin::NativeFunctionDefinition { name: #value, + aliases: &[], func: crate::steel_vm::builtin::BuiltInFunctionType::Reference(#function_name), arity: crate::steel_vm::builtin::Arity::#arity_number, - doc: Some(crate::steel_vm::builtin::MarkdownDoc(#doc)), + doc: Some(crate::steel_vm::builtin::MarkdownDoc::from_str(#doc)), is_const: #is_const, signature: None, }; @@ -195,6 +232,7 @@ pub fn native( quote! { pub const #doc_name: crate::steel_vm::builtin::NativeFunctionDefinition = crate::steel_vm::builtin::NativeFunctionDefinition { name: #value, + aliases: &[], func: crate::steel_vm::builtin::BuiltInFunctionType::Reference(#function_name), arity: crate::steel_vm::builtin::Arity::#arity_number, doc: None, @@ -261,9 +299,10 @@ pub fn native_mut( quote! { pub const #doc_name: crate::steel_vm::builtin::NativeFunctionDefinition = crate::steel_vm::builtin::NativeFunctionDefinition { name: #value, + aliases: &[], func: crate::steel_vm::builtin::BuiltInFunctionType::Mutable(#function_name), arity: crate::steel_vm::builtin::Arity::#arity_number, - doc: Some(crate::steel_vm::builtin::MarkdownDoc(#doc)), + doc: Some(crate::steel_vm::builtin::MarkdownDoc::from_str(#doc)), is_const: #is_const, signature: None, }; @@ -272,6 +311,7 @@ pub fn native_mut( quote! { pub const #doc_name: crate::steel_vm::builtin::NativeFunctionDefinition = crate::steel_vm::builtin::NativeFunctionDefinition { name: #value, + aliases: &[], func: crate::steel_vm::builtin::BuiltInFunctionType::Mutable(#function_name), arity: crate::steel_vm::builtin::Arity::#arity_number, doc: None, @@ -474,13 +514,19 @@ pub fn function( } }; + let aliases = match keyword_map.get("alias") { + Some(alias) => quote! { &[ #alias ] }, + None => quote! { &[] }, + }; + let definition_struct = if let Some(doc) = maybe_doc_comments { quote! { pub const #doc_name: crate::steel_vm::builtin::NativeFunctionDefinition = crate::steel_vm::builtin::NativeFunctionDefinition { name: #value, + aliases: #aliases, func: #function_type, arity: crate::steel_vm::builtin::Arity::#arity_exactness(#arity_number), - doc: Some(crate::steel_vm::builtin::MarkdownDoc(#doc)), + doc: Some(crate::steel_vm::builtin::MarkdownDoc::from_str(#doc)), is_const: #is_const, signature: None, }; @@ -489,6 +535,7 @@ pub fn function( quote! { pub const #doc_name: crate::steel_vm::builtin::NativeFunctionDefinition = crate::steel_vm::builtin::NativeFunctionDefinition { name: #value, + aliases: #aliases, func: #function_type, arity: crate::steel_vm::builtin::Arity::#arity_exactness(#arity_number), doc: None, diff --git a/crates/steel-doc/src/main.rs b/crates/steel-doc/src/main.rs index 0e9baefef..c868b0b03 100644 --- a/crates/steel-doc/src/main.rs +++ b/crates/steel-doc/src/main.rs @@ -37,7 +37,7 @@ fn main() { if let Some(module_doc) = module.documentation().get(&module_name) { if let steel::steel_vm::builtin::Documentation::Markdown(m) = module_doc { - format_markdown_doc(&mut module_file, m.0); + format_markdown_doc(&mut module_file, &m.0); } } @@ -65,7 +65,7 @@ fn main() { let escaped = name.replace("*", "\\*"); writeln!(&mut module_file, "### **{}**", escaped).unwrap(); - format_markdown_doc(&mut module_file, m.0); + format_markdown_doc(&mut module_file, &m.0); } _ => {} } diff --git a/crates/steel-parser/src/lexer.rs b/crates/steel-parser/src/lexer.rs index edcaeee60..e11dd788c 100644 --- a/crates/steel-parser/src/lexer.rs +++ b/crates/steel-parser/src/lexer.rs @@ -152,6 +152,16 @@ impl<'a> Lexer<'a> { "#\\newline" => Some('\n'), "#\\return" => Some('\r'), "#\\RETURN" => Some('\r'), + "#\\NULL" => Some('\0'), + "#\\null" => Some('\0'), + "#\\ALARM" => Some('\x07'), + "#\\alarm" => Some('\x07'), + "#\\BACKSPACE" => Some('\x08'), + "#\\backspace" => Some('\x08'), + "#\\DELETE" => Some('\x7F'), + "#\\delete" => Some('\x7F'), + "#\\ESCAPE" => Some('\x1B'), + "#\\escape" => Some('\x1B'), "#\\)" => Some(')'), "#\\]" => Some(']'), "#\\[" => Some('['), diff --git a/docs/src/builtins/steel_base.md b/docs/src/builtins/steel_base.md index 48601cd74..d3ee59f7f 100644 --- a/docs/src/builtins/steel_base.md +++ b/docs/src/builtins/steel_base.md @@ -125,6 +125,19 @@ Converts the bytevector to the equivalent list representation. ```scheme (bytes->list (bytes 0 1 2 3 4 5)) ;; => '(0 1 2 3 4 5) ``` +### **bytes->string/utf8** +Decodes a string from a bytevector containing valid UTF-8. + +(bytes->string/utf8 buf [start] [end]) -> string? + +* buf : bytes? +* start: int? = 0 +* end: int? = (bytes-length buf) + +#### Examples +```scheme +(bytes->string/utf8 (bytes #xe5 #x8d #x83 #xe8 #x91 #x89)) ;; => "千葉" +``` ### **bytes-append** Append two byte vectors into a new bytevector. @@ -254,11 +267,56 @@ Rounds the given number up to the nearest integer not less than it. > (ceiling 42.1) ;; => 43 > (ceiling -42.1) ;; => -42 ``` +### **char->integer** +Returns the Unicode codepoint of a given character. + +(char->integer char?) -> integer? +### **char->number** +Attemps to convert the character into a decimal digit, +and returns `#f` on failure. +### **char-digit?** +Returns `#t` if the character is a decimal digit. +### **char-downcase** +Returns the lower case version of a character, if defined by Unicode, +or the same character otherwise. +### **char-upcase** +Returns the upper case version of a character, if defined by Unicode, +or the same character otherwise. +### **char-whitespace?** +Returns `#t` if the character is a whitespace character. +### **char<=?** +Compares characters according to their codepoints, in a "less-than-or-equal" fashion. + +(char<=? char1 char2 ... ) -> bool? +* char1 : char? +* char2 : char? +### **char bool? +* char1 : char? +* char2 : char? ### **char=?** -Checks if two characters are equal +Checks if all characters are equal. + +Requires that all inputs are characters, and will otherwise raise an error. + +(char=? char1 char2 ...) -> bool? + +* char1 : char? +* char2 : char? +### **char>=?** +Compares characters according to their codepoints, in a "greater-than-or-equal" fashion. -Requires that the two inputs are both characters, and will otherwise -raise an error. +(char>=? char1 char2 ... ) -> bool? +* char1 : char? +* char2 : char? +### **char>?** +Compares characters according to their codepoints, in a "greater-than" fashion. + +(char>? char1 char2 ... ) -> bool? +* char1 : char? +* char2 : char? ### **complex?** Checks if the given value is a complex number @@ -345,6 +403,14 @@ pattern: string? > (ends-with? "foobar" "foo") ;; => #false > (ends-with? "foobar" "bar") ;; => #true ``` +### **eof-object** +Returns an EOF object. + +(eof-object) -> eof-object? +### **eof-object?** +Returns `#t` if the value is an EOF object. + +(eof-object? any/c) -> bool? ### **exact->inexact** Converts an exact number to an inexact number. @@ -479,6 +545,14 @@ Computes the largest integer less than or equal to the given number. > (floor 4.99) ;; => 4 > (floor -2.5) ;; => -3 ``` +### **get-output-bytevector** +Extracts the contents from a port created with `open-output-bytevector`. + +(get-output-bytevector port?) -> bytes? +### **get-output-string** +Extracts the string contents from a port created with `open-output-string`. + +(get-output-string port?) -> string? ### **hash** Creates an immutable hash table with each given `key` mapped to the following `val`. Each key must have a val, so the total number of arguments must be even. @@ -731,6 +805,10 @@ Checks if the given value is an integer, an alias for `integer?` > (int? 3.14) ;; => #f > (int? "hello") ;; => #f ``` +### **integer->char** +Returns the character corresponding to a given Unicode codepoint. + +(integer->char integer?) -> char? ### **integer?** Checks if the given value is an integer, an alias for `int?` @@ -893,7 +971,7 @@ Checks if the given real number is negative. > (negative? -1) ;; => #t ``` ### **number->string** -Converts the given number to a string +Converts the given number to a string. ### **number?** Checks if the given value is a number @@ -920,6 +998,10 @@ Retrieves the numerator of the given rational number. > (numerator 5/2) ;; => 5 > (numerator -2) ;; => -2 ``` +### **open-input-bytevector** +Creates an input port from a bytevector, that will return the bytevector contents. + +(open-input-bytevector bytes?) -> input-port? ### **open-input-file** Takes a filename `path` referring to an existing file and returns an input port. Raises an error if the file does not exist @@ -936,6 +1018,26 @@ error[E08]: Io 1 │ (open-input-file "foo-bar.txt") │ ^^^^^^^^^^^^^^^ No such file or directory (os error 2) ``` +### **open-input-string** +Creates an input port from a string, that will return the string contents. + +(open-input-string string?) -> input-port? +### **open-output-bytevector** +Creates an output port that accumulates what is written into a bytevector. +These bytes can be recovered calling `get-output-bytevector`. + +(open-output-bytevector) -> output-port? + +#### Examples +```scheme +(define out (open-output-bytevector)) + + +(write-byte 30 out) +(write-byte 250 out) + +(get-output-bytevector out) ;; => (bytes 30 250) +``` ### **open-output-file** Takes a filename `path` referring to a file to be created and returns an output port. @@ -945,6 +1047,22 @@ Takes a filename `path` referring to a file to be created and returns an output ```scheme > (open-output-file "foo-bar.txt") ;; => # ``` +### **open-output-string** +Creates an output port that accumulates what is written into a string. +This string can be recovered calling `get-output-string`. + +(open-output-string) -> output-port? + +#### Examples +```scheme +(define out (open-output-string)) + + +(write-char "α" out) +(write-char "ω" out) + +(get-output-string out) ;; => "αω" +``` ### **output-port?** Checks if a given value is an output port @@ -958,8 +1076,6 @@ Checks if a given value is an output port ``` ### **pair?** Checks if the given value can be treated as a pair. -Note - there are no improper lists in steel, so any list with at least one element -is considered a pair. (pair? any/c) -> bool? @@ -974,6 +1090,12 @@ is considered a pair. Gets the extension from a path ### **path-exists?** Checks if a path exists +### **peek-byte** +Peeks the next byte from an input port. + +(peek-byte [port]) -> byte? + +* port : input-port? = (current-input-port) ### **positive?** Checks if the given real number is positive. @@ -1028,6 +1150,18 @@ Examples: > (rational? 6/10) ;; => #t > (rational? +nan.0) ;; => #f ``` +### **read-byte** +Reads a single byte from an input port. + +(read-byte [port]) -> byte? + +* port : input-port? = (current-input-port) +### **read-char** +Reads the next character from an input port. + +(read-char [port]) -> char? + +* port : input-port? = (current-input-port) ### **read-dir** Returns the contents of the directory as a list ### **read-port-to-string** @@ -1201,7 +1335,9 @@ Gets the port handle to stdin ### **string** Constructs a string from the given characters ### **string->bytes** -Converts the given string to a bytevector +Encodes a string as UTF-8 into a bytevector. + +(string->bytes string?) -> bytes? #### Examples ```scheme @@ -1221,7 +1357,7 @@ Converts a string into an int. Raises an error if the string cannot be converted ### **string->jsexpr** Deserializes a JSON string into a Steel value. -(string->jsexpr json) -> any/c? +(string->jsexpr json) -> any/c * json : string? @@ -1232,7 +1368,11 @@ Deserializes a JSON string into a Steel value. ### **string->list** Converts a string into a list of characters. -(string->list string?) -> (listof char?) +(string->list s [start] [end]) -> (listof char?) + +* s : string? +* start : int? = 0 +* end : int? #### Examples @@ -1251,7 +1391,7 @@ Creates a new lowercased version of the input string ``` ### **string->number** Converts the given string to a number, with an optional radix. -On failure, it returns `f` +On failure, it returns `#f` (string->number digits [radix]) -> (or/c number? boolean?) @@ -1277,6 +1417,17 @@ Creates a new uppercased version of the input string ```scheme > (string->upper "lower") ;; => "LOWER" ``` +### **string->utf8** +Alias of `string->bytes`. +### **string->vector** +Returns a vector containing the characters of a given string + +(string->vector string?) -> vector? + +#### Examples +```scheme +(string->vector "hello") ;; => '#(#\h #\e #\l #\l #\o) +``` ### **string-append** Concatenates all of the given strings into one @@ -1290,18 +1441,35 @@ Concatenates all of the given strings into one > (string-append "foo" "bar") ;; => "foobar" ``` ### **string-ci<=?** -Compares two strings lexicographically (as in "less-than-or-equal"), +Compares strings lexicographically (as in"less-than-or-equal"), +in a case insensitive fashion. + +(string-ci<=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string-ci bool? +* s1 : string? +* s2 : string? ### **string-ci=?** -Compares two strings for equality, in a case insensitive fashion. +Compares strings for equality, in a case insensitive fashion. ### **string-ci>=?** -Compares two strings lexicographically (as in "greater-than-or-equal"), +Compares strings lexicographically (as in"greater-than-or-equal"), in a case insensitive fashion. + +(string-ci>=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string-ci>?** -Compares two strings lexicographically (as in "greater-than"), -in a case-insensitive fashion. +Compares strings lexicographically (as in"greater-than"), +in a case insensitive fashion. + +(string-ci>? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string-length** Get the length of the given string in UTF-8 bytes. @@ -1317,7 +1485,7 @@ Get the length of the given string in UTF-8 bytes. ### **string-ref** Extracts the nth character out of a given string. -(string-ref str n) +(string-ref str n) -> char? * str : string? * n : int? @@ -1335,15 +1503,36 @@ Replaces all occurrences of a pattern into the given string (string-replace "hello world" "o" "@") ;; => "hell@ w@rld" ``` ### **string<=?** -Compares two strings lexicographically (as in "less-than-or-equal"). +Compares strings lexicographically (as in"less-than-equal-to"). + +(string<=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string bool? +* s1 : string? +* s2 : string? ### **string=?** -Compares two strings for equality. +Compares strings for equality. + +(string=? string1 string2 ...) -> bool? + +* string1 : string? +* string2 : string? ### **string>=?** -Compares two strings lexicographically (as in "greater-than-or-equal"). +Compares strings lexicographically (as in"greater-than-or-equal"). + +(string>=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string>?** -Compares two strings lexicographically (as in "greater-than"). +Compares strings lexicographically (as in"greater-than"). + +(string>? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **substring** Creates a substring slicing the characters between two indices. @@ -1462,10 +1651,12 @@ of the string ```scheme > (trim-start-matches "123foo1bar123123" "123") ;; => "foo1bar123123" ``` +### **utf8->string** +Alias of `bytes->string/utf8`. ### **value->jsexpr-string** Serializes a Steel value into a string. -(value->jsexpr-string any/c?) -> string? +(value->jsexpr-string any/c) -> string? #### Examples ```scheme @@ -1473,6 +1664,20 @@ Serializes a Steel value into a string. ``` ### **void** The void value, returned by many forms with side effects, such as `define`. +### **write-byte** +Writes a single byte to an output port. + +(write-byte b [port]) + +* b : byte? +* port : output-port? = (current-output-port) +### **write-bytes** +Writes the contents of a bytevector into an output port. + +(write-bytes buf [port]) + +* buf : bytes? +* port : output-port? = (current-output-port) ### **zero?** Checks if the given real number is zero. @@ -1529,13 +1734,10 @@ Checks if the given real number is zero. ### **channel->recv** ### **channel->send** ### **channel->try-recv** -### **char->number** -### **char-digit?** -### **char-upcase** -### **char-whitespace?** ### **char?** ### **child-stdin** ### **child-stdout** +### **close-output-port** ### **command** ### **compose** ### **concat-symbols** @@ -1566,7 +1768,6 @@ Checks if the given real number is zero. ### **function?** ### **future?** ### **get-contract-struct** -### **get-output-string** ### **get-test-mode** ### **hash-get** ### **hash?** @@ -1621,9 +1822,9 @@ Checks if the given real number is zero. ### **not** ### **null?** ### **odd?** -### **open-output-string** ### **poll!** ### **pop-front** +### **port?** ### **procedure?** ### **push** ### **push-back** @@ -1631,9 +1832,6 @@ Checks if the given real number is zero. ### **raise-error** ### **raise-error-with-span** ### **range-vec** -### **raw-write** -### **raw-write-char** -### **raw-write-string** ### **read!** ### **read-line-from-port** ### **read-to-string** diff --git a/docs/src/builtins/steel_bytevectors.md b/docs/src/builtins/steel_bytevectors.md index acba859f6..f1e7d5b2c 100644 --- a/docs/src/builtins/steel_bytevectors.md +++ b/docs/src/builtins/steel_bytevectors.md @@ -32,6 +32,19 @@ Converts the bytevector to the equivalent list representation. ```scheme (bytes->list (bytes 0 1 2 3 4 5)) ;; => '(0 1 2 3 4 5) ``` +### **bytes->string/utf8** +Decodes a string from a bytevector containing valid UTF-8. + +(bytes->string/utf8 buf [start] [end]) -> string? + +* buf : bytes? +* start: int? = 0 +* end: int? = (bytes-length buf) + +#### Examples +```scheme +(bytes->string/utf8 (bytes #xe5 #x8d #x83 #xe8 #x91 #x89)) ;; => "千葉" +``` ### **bytes-append** Append two byte vectors into a new bytevector. @@ -136,10 +149,5 @@ Creates a bytevector given a length and a default value. ```scheme (make-bytes 6 42) ;; => (bytes 42 42 42 42 42) ``` -### **string->bytes** -Converts the given string to a bytevector - -#### Examples -```scheme -(string->bytes "Apple") ;; => (bytes 65 112 112 108 101) -``` +### **utf8->string** +Alias of `bytes->string/utf8`. diff --git a/docs/src/builtins/steel_identity.md b/docs/src/builtins/steel_identity.md index 6dd4456e3..e1d6681b4 100644 --- a/docs/src/builtins/steel_identity.md +++ b/docs/src/builtins/steel_identity.md @@ -12,6 +12,10 @@ Checks if the given value is a complex number > (complex? 42) ;; => #t > (complex? "hello") ;; => #f ``` +### **eof-object?** +Returns `#t` if the value is an EOF object. + +(eof-object? any/c) -> bool? ### **exact-integer?** Checks if the given value is an exact integer @@ -117,6 +121,7 @@ Checks if the given value is a real number ### **list?** ### **mutable-vector?** ### **not** +### **port?** ### **procedure?** ### **set?** ### **string?** diff --git a/docs/src/builtins/steel_json.md b/docs/src/builtins/steel_json.md index 940651420..5f84fae30 100644 --- a/docs/src/builtins/steel_json.md +++ b/docs/src/builtins/steel_json.md @@ -3,7 +3,7 @@ De/serialization from/to JSON. ### **string->jsexpr** Deserializes a JSON string into a Steel value. -(string->jsexpr json) -> any/c? +(string->jsexpr json) -> any/c * json : string? @@ -14,7 +14,7 @@ Deserializes a JSON string into a Steel value. ### **value->jsexpr-string** Serializes a Steel value into a string. -(value->jsexpr-string any/c?) -> string? +(value->jsexpr-string any/c) -> string? #### Examples ```scheme diff --git a/docs/src/builtins/steel_lists.md b/docs/src/builtins/steel_lists.md index 61ae4a7a9..de8483fd9 100644 --- a/docs/src/builtins/steel_lists.md +++ b/docs/src/builtins/steel_lists.md @@ -168,8 +168,6 @@ error[E11]: Generic ``` ### **pair?** Checks if the given value can be treated as a pair. -Note - there are no improper lists in steel, so any list with at least one element -is considered a pair. (pair? any/c) -> bool? diff --git a/docs/src/builtins/steel_ports.md b/docs/src/builtins/steel_ports.md index a693c5035..7346041c1 100644 --- a/docs/src/builtins/steel_ports.md +++ b/docs/src/builtins/steel_ports.md @@ -1,4 +1,16 @@ # steel/ports +### **eof-object** +Returns an EOF object. + +(eof-object) -> eof-object? +### **get-output-bytevector** +Extracts the contents from a port created with `open-output-bytevector`. + +(get-output-bytevector port?) -> bytes? +### **get-output-string** +Extracts the string contents from a port created with `open-output-string`. + +(get-output-string port?) -> string? ### **input-port?** Checks if a given value is an input port @@ -10,6 +22,10 @@ Checks if a given value is an input port > (input-port? (stdin)) ;; => #true > (input-port? "foo") ;; => #false ``` +### **open-input-bytevector** +Creates an input port from a bytevector, that will return the bytevector contents. + +(open-input-bytevector bytes?) -> input-port? ### **open-input-file** Takes a filename `path` referring to an existing file and returns an input port. Raises an error if the file does not exist @@ -26,6 +42,26 @@ error[E08]: Io 1 │ (open-input-file "foo-bar.txt") │ ^^^^^^^^^^^^^^^ No such file or directory (os error 2) ``` +### **open-input-string** +Creates an input port from a string, that will return the string contents. + +(open-input-string string?) -> input-port? +### **open-output-bytevector** +Creates an output port that accumulates what is written into a bytevector. +These bytes can be recovered calling `get-output-bytevector`. + +(open-output-bytevector) -> output-port? + +#### Examples +```scheme +(define out (open-output-bytevector)) + + +(write-byte 30 out) +(write-byte 250 out) + +(get-output-bytevector out) ;; => (bytes 30 250) +``` ### **open-output-file** Takes a filename `path` referring to a file to be created and returns an output port. @@ -35,6 +71,22 @@ Takes a filename `path` referring to a file to be created and returns an output ```scheme > (open-output-file "foo-bar.txt") ;; => # ``` +### **open-output-string** +Creates an output port that accumulates what is written into a string. +This string can be recovered calling `get-output-string`. + +(open-output-string) -> output-port? + +#### Examples +```scheme +(define out (open-output-string)) + + +(write-char "α" out) +(write-char "ω" out) + +(get-output-string out) ;; => "αω" +``` ### **output-port?** Checks if a given value is an output port @@ -46,6 +98,24 @@ Checks if a given value is an output port > (define output (open-output-file "foo.txt")) > (output-port? output) ;; => #true ``` +### **peek-byte** +Peeks the next byte from an input port. + +(peek-byte [port]) -> byte? + +* port : input-port? = (current-input-port) +### **read-byte** +Reads a single byte from an input port. + +(read-byte [port]) -> byte? + +* port : input-port? = (current-input-port) +### **read-char** +Reads the next character from an input port. + +(read-char [port]) -> char? + +* port : input-port? = (current-input-port) ### **read-port-to-string** Takes a port and reads the entire content into a string @@ -62,12 +132,22 @@ Gets the port handle to stdin ```scheme > (stdin) ;; => # ``` +### **write-byte** +Writes a single byte to an output port. + +(write-byte b [port]) + +* b : byte? +* port : output-port? = (current-output-port) +### **write-bytes** +Writes the contents of a bytevector into an output port. + +(write-bytes buf [port]) + +* buf : bytes? +* port : output-port? = (current-output-port) +### **close-output-port** ### **flush-output-port** -### **get-output-string** -### **open-output-string** -### **raw-write** -### **raw-write-char** -### **raw-write-string** ### **read-line-from-port** ### **stdout** ### **write-line!** diff --git a/docs/src/builtins/steel_strings.md b/docs/src/builtins/steel_strings.md index ff8be52d6..45abe5823 100644 --- a/docs/src/builtins/steel_strings.md +++ b/docs/src/builtins/steel_strings.md @@ -2,11 +2,56 @@ Strings in Steel are immutable, fixed length arrays of characters. They are heap allocated, and are implemented under the hood as referenced counted Rust `Strings`. Rust `Strings` are stored as UTF-8 encoded bytes. +### **char->integer** +Returns the Unicode codepoint of a given character. + +(char->integer char?) -> integer? +### **char->number** +Attemps to convert the character into a decimal digit, +and returns `#f` on failure. +### **char-digit?** +Returns `#t` if the character is a decimal digit. +### **char-downcase** +Returns the lower case version of a character, if defined by Unicode, +or the same character otherwise. +### **char-upcase** +Returns the upper case version of a character, if defined by Unicode, +or the same character otherwise. +### **char-whitespace?** +Returns `#t` if the character is a whitespace character. +### **char<=?** +Compares characters according to their codepoints, in a "less-than-or-equal" fashion. + +(char<=? char1 char2 ... ) -> bool? +* char1 : char? +* char2 : char? +### **char bool? +* char1 : char? +* char2 : char? ### **char=?** -Checks if two characters are equal +Checks if all characters are equal. -Requires that the two inputs are both characters, and will otherwise -raise an error. +Requires that all inputs are characters, and will otherwise raise an error. + +(char=? char1 char2 ...) -> bool? + +* char1 : char? +* char2 : char? +### **char>=?** +Compares characters according to their codepoints, in a "greater-than-or-equal" fashion. + +(char>=? char1 char2 ... ) -> bool? +* char1 : char? +* char2 : char? +### **char>?** +Compares characters according to their codepoints, in a "greater-than" fashion. + +(char>? char1 char2 ... ) -> bool? +* char1 : char? +* char2 : char? ### **ends-with?** Checks if the input string ends with a given suffix @@ -31,6 +76,10 @@ Converts an integer into a string. ```scheme > (int->string 10) ;; => "10" ``` +### **integer->char** +Returns the character corresponding to a given Unicode codepoint. + +(integer->char integer?) -> char? ### **make-string** Creates a string of a given length, filled with an optional character (which defaults to `#\0`). @@ -40,7 +89,7 @@ Creates a string of a given length, filled with an optional character * len : int? * char : char? = #\0 ### **number->string** -Converts the given number to a string +Converts the given number to a string. ### **split-many** Splits a string given a separator pattern into a list of strings. @@ -96,6 +145,15 @@ Checks if the input string starts with a prefix ``` ### **string** Constructs a string from the given characters +### **string->bytes** +Encodes a string as UTF-8 into a bytevector. + +(string->bytes string?) -> bytes? + +#### Examples +```scheme +(string->bytes "Apple") ;; => (bytes 65 112 112 108 101) +``` ### **string->int** Converts a string into an int. Raises an error if the string cannot be converted to an integer. @@ -110,7 +168,11 @@ Converts a string into an int. Raises an error if the string cannot be converted ### **string->list** Converts a string into a list of characters. -(string->list string?) -> (listof char?) +(string->list s [start] [end]) -> (listof char?) + +* s : string? +* start : int? = 0 +* end : int? #### Examples @@ -129,7 +191,7 @@ Creates a new lowercased version of the input string ``` ### **string->number** Converts the given string to a number, with an optional radix. -On failure, it returns `f` +On failure, it returns `#f` (string->number digits [radix]) -> (or/c number? boolean?) @@ -155,6 +217,17 @@ Creates a new uppercased version of the input string ```scheme > (string->upper "lower") ;; => "LOWER" ``` +### **string->utf8** +Alias of `string->bytes`. +### **string->vector** +Returns a vector containing the characters of a given string + +(string->vector string?) -> vector? + +#### Examples +```scheme +(string->vector "hello") ;; => '#(#\h #\e #\l #\l #\o) +``` ### **string-append** Concatenates all of the given strings into one @@ -168,18 +241,35 @@ Concatenates all of the given strings into one > (string-append "foo" "bar") ;; => "foobar" ``` ### **string-ci<=?** -Compares two strings lexicographically (as in "less-than-or-equal"), +Compares strings lexicographically (as in"less-than-or-equal"), +in a case insensitive fashion. + +(string-ci<=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string-ci bool? +* s1 : string? +* s2 : string? ### **string-ci=?** -Compares two strings for equality, in a case insensitive fashion. +Compares strings for equality, in a case insensitive fashion. ### **string-ci>=?** -Compares two strings lexicographically (as in "greater-than-or-equal"), +Compares strings lexicographically (as in"greater-than-or-equal"), in a case insensitive fashion. + +(string-ci>=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string-ci>?** -Compares two strings lexicographically (as in "greater-than"), -in a case-insensitive fashion. +Compares strings lexicographically (as in"greater-than"), +in a case insensitive fashion. + +(string-ci>? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string-length** Get the length of the given string in UTF-8 bytes. @@ -195,7 +285,7 @@ Get the length of the given string in UTF-8 bytes. ### **string-ref** Extracts the nth character out of a given string. -(string-ref str n) +(string-ref str n) -> char? * str : string? * n : int? @@ -213,15 +303,36 @@ Replaces all occurrences of a pattern into the given string (string-replace "hello world" "o" "@") ;; => "hell@ w@rld" ``` ### **string<=?** -Compares two strings lexicographically (as in "less-than-or-equal"). +Compares strings lexicographically (as in"less-than-equal-to"). + +(string<=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string bool? +* s1 : string? +* s2 : string? ### **string=?** -Compares two strings for equality. +Compares strings for equality. + +(string=? string1 string2 ...) -> bool? + +* string1 : string? +* string2 : string? ### **string>=?** -Compares two strings lexicographically (as in "greater-than-or-equal"). +Compares strings lexicographically (as in"greater-than-or-equal"). + +(string>=? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **string>?** -Compares two strings lexicographically (as in "greater-than"). +Compares strings lexicographically (as in"greater-than"). + +(string>? s1 s2 ... ) -> bool? +* s1 : string? +* s2 : string? ### **substring** Creates a substring slicing the characters between two indices. @@ -303,7 +414,3 @@ of the string ```scheme > (trim-start-matches "123foo1bar123123" "123") ;; => "foo1bar123123" ``` -### **char->number** -### **char-digit?** -### **char-upcase** -### **char-whitespace?**