difftreelog
feat add intrinsics for numeric parsing
in: master
2 files changed
crates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth1use std::{2 cell::{Ref, RefCell, RefMut},3 collections::HashMap,4 rc::Rc,5};67use jrsonnet_evaluator::{8 error::{ErrorKind::*, Result},9 function::{builtin::Builtin, CallLocation, FuncVal, TlaArg},10 gc::{GcHashMap, TraceBox},11 tb,12 trace::PathResolver,13 Context, ContextBuilder, IStr, ObjValue, ObjValueBuilder, State, Thunk, Val,14};15use jrsonnet_gcmodule::{Cc, Trace};16use jrsonnet_parser::Source;1718mod expr;19mod types;20pub use types::*;21mod arrays;22pub use arrays::*;23mod math;24pub use math::*;25mod operator;26pub use operator::*;27mod sort;28pub use sort::*;29mod hash;30pub use hash::*;31mod encoding;32pub use encoding::*;33mod objects;34pub use objects::*;35mod manifest;36pub use manifest::*;37mod parse;38pub use parse::*;39mod strings;40pub use strings::*;41mod misc;42pub use misc::*;4344pub fn stdlib_uncached(settings: Rc<RefCell<Settings>>) -> ObjValue {45 let mut builder = ObjValueBuilder::new();4647 let expr = expr::stdlib_expr();48 let eval = jrsonnet_evaluator::evaluate(ContextBuilder::dangerous_empty_state().build(), &expr)49 .expect("stdlib.jsonnet should have no errors")50 .as_obj()51 .expect("stdlib.jsonnet should evaluate to object");5253 builder.with_super(eval);5455 for (name, builtin) in [56 // Types57 ("type", builtin_type::INST),58 ("isString", builtin_is_string::INST),59 ("isNumber", builtin_is_number::INST),60 ("isBoolean", builtin_is_boolean::INST),61 ("isObject", builtin_is_object::INST),62 ("isArray", builtin_is_array::INST),63 ("isFunction", builtin_is_function::INST),64 // Arrays65 ("makeArray", builtin_make_array::INST),66 ("slice", builtin_slice::INST),67 ("map", builtin_map::INST),68 ("flatMap", builtin_flatmap::INST),69 ("filter", builtin_filter::INST),70 ("foldl", builtin_foldl::INST),71 ("foldr", builtin_foldr::INST),72 ("range", builtin_range::INST),73 ("join", builtin_join::INST),74 ("reverse", builtin_reverse::INST),75 ("any", builtin_any::INST),76 ("all", builtin_all::INST),77 ("member", builtin_member::INST),78 ("count", builtin_count::INST),79 // Math80 ("modulo", builtin_modulo::INST),81 ("floor", builtin_floor::INST),82 ("ceil", builtin_ceil::INST),83 ("log", builtin_log::INST),84 ("pow", builtin_pow::INST),85 ("sqrt", builtin_sqrt::INST),86 ("sin", builtin_sin::INST),87 ("cos", builtin_cos::INST),88 ("tan", builtin_tan::INST),89 ("asin", builtin_asin::INST),90 ("acos", builtin_acos::INST),91 ("atan", builtin_atan::INST),92 ("exp", builtin_exp::INST),93 ("mantissa", builtin_mantissa::INST),94 ("exponent", builtin_exponent::INST),95 // Operator96 ("mod", builtin_mod::INST),97 ("primitiveEquals", builtin_primitive_equals::INST),98 ("equals", builtin_equals::INST),99 ("format", builtin_format::INST),100 // Sort101 ("sort", builtin_sort::INST),102 // Hash103 ("md5", builtin_md5::INST),104 #[cfg(feature = "exp-more-hashes")]105 ("sha256", builtin_sha256::INST),106 // Encoding107 ("encodeUTF8", builtin_encode_utf8::INST),108 ("decodeUTF8", builtin_decode_utf8::INST),109 ("base64", builtin_base64::INST),110 ("base64Decode", builtin_base64_decode::INST),111 ("base64DecodeBytes", builtin_base64_decode_bytes::INST),112 // Objects113 ("objectFieldsEx", builtin_object_fields_ex::INST),114 ("objectHasEx", builtin_object_has_ex::INST),115 // Manifest116 ("escapeStringJson", builtin_escape_string_json::INST),117 ("manifestJsonEx", builtin_manifest_json_ex::INST),118 ("manifestYamlDoc", builtin_manifest_yaml_doc::INST),119 // Parsing120 ("parseJson", builtin_parse_json::INST),121 ("parseYaml", builtin_parse_yaml::INST),122 // Strings123 ("codepoint", builtin_codepoint::INST),124 ("substr", builtin_substr::INST),125 ("char", builtin_char::INST),126 ("strReplace", builtin_str_replace::INST),127 ("splitLimit", builtin_splitlimit::INST),128 ("asciiUpper", builtin_ascii_upper::INST),129 ("asciiLower", builtin_ascii_lower::INST),130 ("findSubstr", builtin_find_substr::INST),131 // Misc132 ("length", builtin_length::INST),133 ("startsWith", builtin_starts_with::INST),134 ("endsWith", builtin_ends_with::INST),135 ]136 .iter()137 .cloned()138 {139 builder140 .member(name.into())141 .hide()142 .value(Val::Func(FuncVal::StaticBuiltin(builtin)))143 .expect("no conflict");144 }145146 builder147 .member("extVar".into())148 .hide()149 .value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_ext_var {150 settings: settings.clone()151 })))))152 .expect("no conflict");153 builder154 .member("native".into())155 .hide()156 .value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_native {157 settings: settings.clone()158 })))))159 .expect("no conflict");160 builder161 .member("trace".into())162 .hide()163 .value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_trace {164 settings165 })))))166 .expect("no conflict");167168 builder169 .member("id".into())170 .hide()171 .value(Val::Func(FuncVal::Id))172 .expect("no conflict");173174 builder.build()175}176177pub trait TracePrinter {178 fn print_trace(&self, loc: CallLocation, value: IStr);179}180181pub struct StdTracePrinter {182 resolver: PathResolver,183}184impl StdTracePrinter {185 pub fn new(resolver: PathResolver) -> Self {186 Self { resolver }187 }188}189impl TracePrinter for StdTracePrinter {190 fn print_trace(&self, loc: CallLocation, value: IStr) {191 eprint!("TRACE:");192 if let Some(loc) = loc.0 {193 let locs = loc.0.map_source_locations(&[loc.1]);194 eprint!(195 " {}:{}",196 match loc.0.source_path().path() {197 Some(p) => self.resolver.resolve(p),198 None => loc.0.source_path().to_string(),199 },200 locs[0].line201 );202 }203 eprintln!(" {}", value);204 }205}206207pub struct Settings {208 /// Used for `std.extVar`209 pub ext_vars: HashMap<IStr, TlaArg>,210 /// Used for `std.native`211 pub ext_natives: HashMap<IStr, Cc<TraceBox<dyn Builtin>>>,212 /// Helper to add globals without implementing custom ContextInitializer213 pub globals: GcHashMap<IStr, Thunk<Val>>,214 /// Used for `std.trace`215 pub trace_printer: Box<dyn TracePrinter>,216 /// Used for `std.thisFile`217 pub path_resolver: PathResolver,218}219220fn extvar_source(name: &str, code: impl Into<IStr>) -> Source {221 let source_name = format!("<extvar:{}>", name);222 Source::new_virtual(source_name.into(), code.into())223}224225#[derive(Trace)]226pub struct ContextInitializer {227 // When we don't need to support legacy-this-file, we can reuse same context for all files228 #[cfg(not(feature = "legacy-this-file"))]229 context: Context,230 // Otherwise, we can only keep first stdlib layer, and then stack thisFile on top of it231 #[cfg(feature = "legacy-this-file")]232 stdlib_obj: ObjValue,233 settings: Rc<RefCell<Settings>>,234}235impl ContextInitializer {236 pub fn new(s: State, resolver: PathResolver) -> Self {237 let settings = Settings {238 ext_vars: Default::default(),239 ext_natives: Default::default(),240 globals: Default::default(),241 trace_printer: Box::new(StdTracePrinter::new(resolver.clone())),242 path_resolver: resolver,243 };244 let settings = Rc::new(RefCell::new(settings));245 Self {246 #[cfg(not(feature = "legacy-this-file"))]247 context: {248 let mut context = ContextBuilder::with_capacity(s, 1);249 context.bind(250 "std".into(),251 Thunk::evaluated(Val::Obj(stdlib_uncached(settings.clone()))),252 );253 context.build()254 },255 #[cfg(feature = "legacy-this-file")]256 stdlib_obj: stdlib_uncached(settings.clone()),257 settings,258 }259 }260 pub fn settings(&self) -> Ref<Settings> {261 self.settings.borrow()262 }263 pub fn settings_mut(&self) -> RefMut<Settings> {264 self.settings.borrow_mut()265 }266 pub fn add_ext_var(&self, name: IStr, value: Val) {267 self.settings_mut()268 .ext_vars269 .insert(name, TlaArg::Val(value));270 }271 pub fn add_ext_str(&self, name: IStr, value: IStr) {272 self.settings_mut()273 .ext_vars274 .insert(name, TlaArg::String(value));275 }276 pub fn add_ext_code(&self, name: &str, code: impl Into<IStr>) -> Result<()> {277 let code = code.into();278 let source = extvar_source(name, code.clone());279 let parsed = jrsonnet_parser::parse(280 &code,281 &jrsonnet_parser::ParserSettings {282 source: source.clone(),283 },284 )285 .map_err(|e| ImportSyntaxError {286 path: source,287 error: Box::new(e),288 })?;289 // self.data_mut().volatile_files.insert(source_name, code);290 self.settings_mut()291 .ext_vars292 .insert(name.into(), TlaArg::Code(parsed));293 Ok(())294 }295 pub fn add_native(&self, name: IStr, cb: Cc<TraceBox<dyn Builtin>>) {296 self.settings_mut().ext_natives.insert(name, cb);297 }298}299impl jrsonnet_evaluator::ContextInitializer for ContextInitializer {300 #[cfg(not(feature = "legacy-this-file"))]301 fn initialize(&self, _s: State, _source: Source) -> jrsonnet_evaluator::Context {302 let out = self.context.clone();303 let globals = &self.settings().globals;304 if globals.is_empty() {305 return out;306 }307308 let mut out = ContextBuilder::extend(out);309 for (k, v) in globals.iter() {310 out.bind(k.clone(), v.clone());311 }312 out.build()313 }314 #[cfg(feature = "legacy-this-file")]315 fn initialize(&self, s: State, source: Source) -> jrsonnet_evaluator::Context {316 let mut builder = ObjValueBuilder::new();317 builder.with_super(self.stdlib_obj.clone());318 builder319 .member("thisFile".into())320 .hide()321 .value(Val::Str(match source.source_path().path() {322 Some(p) => self.settings().path_resolver.resolve(p).into(),323 None => source.source_path().to_string().into(),324 }))325 .expect("this object builder is empty");326 let stdlib_with_this_file = builder.build();327328 let mut context = ContextBuilder::with_capacity(s, 1);329 context.bind(330 "std".into(),331 Thunk::evaluated(Val::Obj(stdlib_with_this_file)),332 );333 for (k, v) in self.settings().globals.iter() {334 context.bind(k.clone(), v.clone());335 }336 context.build()337 }338 fn as_any(&self) -> &dyn std::any::Any {339 self340 }341}342343pub trait StateExt {344 /// This method was previously implemented in jrsonnet-evaluator itself345 fn with_stdlib(&self);346 fn add_global(&self, name: IStr, value: Thunk<Val>);347}348349impl StateExt for State {350 fn with_stdlib(&self) {351 let initializer = ContextInitializer::new(self.clone(), PathResolver::new_cwd_fallback());352 self.settings_mut().context_initializer = tb!(initializer)353 }354 fn add_global(&self, name: IStr, value: Thunk<Val>) {355 self.settings()356 .context_initializer357 .as_any()358 .downcast_ref::<ContextInitializer>()359 .expect("not standard context initializer")360 .settings_mut()361 .globals362 .insert(name, value);363 }364}1use std::{2 cell::{Ref, RefCell, RefMut},3 collections::HashMap,4 rc::Rc,5};67use jrsonnet_evaluator::{8 error::{ErrorKind::*, Result},9 function::{builtin::Builtin, CallLocation, FuncVal, TlaArg},10 gc::{GcHashMap, TraceBox},11 tb,12 trace::PathResolver,13 Context, ContextBuilder, IStr, ObjValue, ObjValueBuilder, State, Thunk, Val,14};15use jrsonnet_gcmodule::{Cc, Trace};16use jrsonnet_parser::Source;1718mod expr;19mod types;20pub use types::*;21mod arrays;22pub use arrays::*;23mod math;24pub use math::*;25mod operator;26pub use operator::*;27mod sort;28pub use sort::*;29mod hash;30pub use hash::*;31mod encoding;32pub use encoding::*;33mod objects;34pub use objects::*;35mod manifest;36pub use manifest::*;37mod parse;38pub use parse::*;39mod strings;40pub use strings::*;41mod misc;42pub use misc::*;4344pub fn stdlib_uncached(settings: Rc<RefCell<Settings>>) -> ObjValue {45 let mut builder = ObjValueBuilder::new();4647 let expr = expr::stdlib_expr();48 let eval = jrsonnet_evaluator::evaluate(ContextBuilder::dangerous_empty_state().build(), &expr)49 .expect("stdlib.jsonnet should have no errors")50 .as_obj()51 .expect("stdlib.jsonnet should evaluate to object");5253 builder.with_super(eval);5455 for (name, builtin) in [56 // Types57 ("type", builtin_type::INST),58 ("isString", builtin_is_string::INST),59 ("isNumber", builtin_is_number::INST),60 ("isBoolean", builtin_is_boolean::INST),61 ("isObject", builtin_is_object::INST),62 ("isArray", builtin_is_array::INST),63 ("isFunction", builtin_is_function::INST),64 // Arrays65 ("makeArray", builtin_make_array::INST),66 ("slice", builtin_slice::INST),67 ("map", builtin_map::INST),68 ("flatMap", builtin_flatmap::INST),69 ("filter", builtin_filter::INST),70 ("foldl", builtin_foldl::INST),71 ("foldr", builtin_foldr::INST),72 ("range", builtin_range::INST),73 ("join", builtin_join::INST),74 ("reverse", builtin_reverse::INST),75 ("any", builtin_any::INST),76 ("all", builtin_all::INST),77 ("member", builtin_member::INST),78 ("count", builtin_count::INST),79 // Math80 ("modulo", builtin_modulo::INST),81 ("floor", builtin_floor::INST),82 ("ceil", builtin_ceil::INST),83 ("log", builtin_log::INST),84 ("pow", builtin_pow::INST),85 ("sqrt", builtin_sqrt::INST),86 ("sin", builtin_sin::INST),87 ("cos", builtin_cos::INST),88 ("tan", builtin_tan::INST),89 ("asin", builtin_asin::INST),90 ("acos", builtin_acos::INST),91 ("atan", builtin_atan::INST),92 ("exp", builtin_exp::INST),93 ("mantissa", builtin_mantissa::INST),94 ("exponent", builtin_exponent::INST),95 // Operator96 ("mod", builtin_mod::INST),97 ("primitiveEquals", builtin_primitive_equals::INST),98 ("equals", builtin_equals::INST),99 ("format", builtin_format::INST),100 // Sort101 ("sort", builtin_sort::INST),102 // Hash103 ("md5", builtin_md5::INST),104 #[cfg(feature = "exp-more-hashes")]105 ("sha256", builtin_sha256::INST),106 // Encoding107 ("encodeUTF8", builtin_encode_utf8::INST),108 ("decodeUTF8", builtin_decode_utf8::INST),109 ("base64", builtin_base64::INST),110 ("base64Decode", builtin_base64_decode::INST),111 ("base64DecodeBytes", builtin_base64_decode_bytes::INST),112 // Objects113 ("objectFieldsEx", builtin_object_fields_ex::INST),114 ("objectHasEx", builtin_object_has_ex::INST),115 // Manifest116 ("escapeStringJson", builtin_escape_string_json::INST),117 ("manifestJsonEx", builtin_manifest_json_ex::INST),118 ("manifestYamlDoc", builtin_manifest_yaml_doc::INST),119 // Parsing120 ("parseJson", builtin_parse_json::INST),121 ("parseYaml", builtin_parse_yaml::INST),122 // Strings123 ("codepoint", builtin_codepoint::INST),124 ("substr", builtin_substr::INST),125 ("char", builtin_char::INST),126 ("strReplace", builtin_str_replace::INST),127 ("splitLimit", builtin_splitlimit::INST),128 ("asciiUpper", builtin_ascii_upper::INST),129 ("asciiLower", builtin_ascii_lower::INST),130 ("findSubstr", builtin_find_substr::INST),131 ("parseInt", builtin_parse_int::INST),132 ("parseOctal", builtin_parse_octal::INST),133 ("parseHex", builtin_parse_hex::INST),134 // Misc135 ("length", builtin_length::INST),136 ("startsWith", builtin_starts_with::INST),137 ("endsWith", builtin_ends_with::INST),138 ]139 .iter()140 .cloned()141 {142 builder143 .member(name.into())144 .hide()145 .value(Val::Func(FuncVal::StaticBuiltin(builtin)))146 .expect("no conflict");147 }148149 builder150 .member("extVar".into())151 .hide()152 .value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_ext_var {153 settings: settings.clone()154 })))))155 .expect("no conflict");156 builder157 .member("native".into())158 .hide()159 .value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_native {160 settings: settings.clone()161 })))))162 .expect("no conflict");163 builder164 .member("trace".into())165 .hide()166 .value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_trace {167 settings168 })))))169 .expect("no conflict");170171 builder172 .member("id".into())173 .hide()174 .value(Val::Func(FuncVal::Id))175 .expect("no conflict");176177 builder.build()178}179180pub trait TracePrinter {181 fn print_trace(&self, loc: CallLocation, value: IStr);182}183184pub struct StdTracePrinter {185 resolver: PathResolver,186}187impl StdTracePrinter {188 pub fn new(resolver: PathResolver) -> Self {189 Self { resolver }190 }191}192impl TracePrinter for StdTracePrinter {193 fn print_trace(&self, loc: CallLocation, value: IStr) {194 eprint!("TRACE:");195 if let Some(loc) = loc.0 {196 let locs = loc.0.map_source_locations(&[loc.1]);197 eprint!(198 " {}:{}",199 match loc.0.source_path().path() {200 Some(p) => self.resolver.resolve(p),201 None => loc.0.source_path().to_string(),202 },203 locs[0].line204 );205 }206 eprintln!(" {}", value);207 }208}209210pub struct Settings {211 /// Used for `std.extVar`212 pub ext_vars: HashMap<IStr, TlaArg>,213 /// Used for `std.native`214 pub ext_natives: HashMap<IStr, Cc<TraceBox<dyn Builtin>>>,215 /// Helper to add globals without implementing custom ContextInitializer216 pub globals: GcHashMap<IStr, Thunk<Val>>,217 /// Used for `std.trace`218 pub trace_printer: Box<dyn TracePrinter>,219 /// Used for `std.thisFile`220 pub path_resolver: PathResolver,221}222223fn extvar_source(name: &str, code: impl Into<IStr>) -> Source {224 let source_name = format!("<extvar:{}>", name);225 Source::new_virtual(source_name.into(), code.into())226}227228#[derive(Trace)]229pub struct ContextInitializer {230 // When we don't need to support legacy-this-file, we can reuse same context for all files231 #[cfg(not(feature = "legacy-this-file"))]232 context: Context,233 // Otherwise, we can only keep first stdlib layer, and then stack thisFile on top of it234 #[cfg(feature = "legacy-this-file")]235 stdlib_obj: ObjValue,236 settings: Rc<RefCell<Settings>>,237}238impl ContextInitializer {239 pub fn new(s: State, resolver: PathResolver) -> Self {240 let settings = Settings {241 ext_vars: Default::default(),242 ext_natives: Default::default(),243 globals: Default::default(),244 trace_printer: Box::new(StdTracePrinter::new(resolver.clone())),245 path_resolver: resolver,246 };247 let settings = Rc::new(RefCell::new(settings));248 Self {249 #[cfg(not(feature = "legacy-this-file"))]250 context: {251 let mut context = ContextBuilder::with_capacity(s, 1);252 context.bind(253 "std".into(),254 Thunk::evaluated(Val::Obj(stdlib_uncached(settings.clone()))),255 );256 context.build()257 },258 #[cfg(feature = "legacy-this-file")]259 stdlib_obj: stdlib_uncached(settings.clone()),260 settings,261 }262 }263 pub fn settings(&self) -> Ref<Settings> {264 self.settings.borrow()265 }266 pub fn settings_mut(&self) -> RefMut<Settings> {267 self.settings.borrow_mut()268 }269 pub fn add_ext_var(&self, name: IStr, value: Val) {270 self.settings_mut()271 .ext_vars272 .insert(name, TlaArg::Val(value));273 }274 pub fn add_ext_str(&self, name: IStr, value: IStr) {275 self.settings_mut()276 .ext_vars277 .insert(name, TlaArg::String(value));278 }279 pub fn add_ext_code(&self, name: &str, code: impl Into<IStr>) -> Result<()> {280 let code = code.into();281 let source = extvar_source(name, code.clone());282 let parsed = jrsonnet_parser::parse(283 &code,284 &jrsonnet_parser::ParserSettings {285 source: source.clone(),286 },287 )288 .map_err(|e| ImportSyntaxError {289 path: source,290 error: Box::new(e),291 })?;292 // self.data_mut().volatile_files.insert(source_name, code);293 self.settings_mut()294 .ext_vars295 .insert(name.into(), TlaArg::Code(parsed));296 Ok(())297 }298 pub fn add_native(&self, name: IStr, cb: Cc<TraceBox<dyn Builtin>>) {299 self.settings_mut().ext_natives.insert(name, cb);300 }301}302impl jrsonnet_evaluator::ContextInitializer for ContextInitializer {303 #[cfg(not(feature = "legacy-this-file"))]304 fn initialize(&self, _s: State, _source: Source) -> jrsonnet_evaluator::Context {305 let out = self.context.clone();306 let globals = &self.settings().globals;307 if globals.is_empty() {308 return out;309 }310311 let mut out = ContextBuilder::extend(out);312 for (k, v) in globals.iter() {313 out.bind(k.clone(), v.clone());314 }315 out.build()316 }317 #[cfg(feature = "legacy-this-file")]318 fn initialize(&self, s: State, source: Source) -> Context {319 let mut builder = ObjValueBuilder::new();320 builder.with_super(self.stdlib_obj.clone());321 builder322 .member("thisFile".into())323 .hide()324 .value(Val::Str(match source.source_path().path() {325 Some(p) => self.settings().path_resolver.resolve(p).into(),326 None => source.source_path().to_string().into(),327 }))328 .expect("this object builder is empty");329 let stdlib_with_this_file = builder.build();330331 let mut context = ContextBuilder::with_capacity(s, 1);332 context.bind(333 "std".into(),334 Thunk::evaluated(Val::Obj(stdlib_with_this_file)),335 );336 for (k, v) in self.settings().globals.iter() {337 context.bind(k.clone(), v.clone());338 }339 context.build()340 }341 fn as_any(&self) -> &dyn std::any::Any {342 self343 }344}345346pub trait StateExt {347 /// This method was previously implemented in jrsonnet-evaluator itself348 fn with_stdlib(&self);349 fn add_global(&self, name: IStr, value: Thunk<Val>);350}351352impl StateExt for State {353 fn with_stdlib(&self) {354 let initializer = ContextInitializer::new(self.clone(), PathResolver::new_cwd_fallback());355 self.settings_mut().context_initializer = tb!(initializer)356 }357 fn add_global(&self, name: IStr, value: Thunk<Val>) {358 self.settings()359 .context_initializer360 .as_any()361 .downcast_ref::<ContextInitializer>()362 .expect("not standard context initializer")363 .settings_mut()364 .globals365 .insert(name, value);366 }367}crates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/strings.rs
+++ b/crates/jrsonnet-stdlib/src/strings.rs
@@ -1,6 +1,7 @@
use jrsonnet_evaluator::{
error::{ErrorKind::*, Result},
function::builtin,
+ throw,
typed::{Either2, VecVal, M1},
val::ArrValue,
Either, IStr, Val,
@@ -73,3 +74,91 @@
}
Ok(out.into())
}
+
+#[builtin]
+pub fn builtin_parse_int(raw: IStr) -> Result<f64> {
+ let mut chars = raw.chars();
+ if let Some(first_char) = chars.next() {
+ if first_char == '-' {
+ let remaining = chars.as_str();
+ if remaining.is_empty() {
+ throw!("Not an integer: \"{}\"", raw);
+ }
+ parse_nat::<10>(remaining).map(|value| -value)
+ } else {
+ parse_nat::<10>(raw.as_str())
+ }
+ } else {
+ throw!("Not an integer: \"{}\"", raw);
+ }
+}
+
+#[builtin]
+pub fn builtin_parse_octal(raw: IStr) -> Result<f64> {
+ if raw.is_empty() {
+ throw!("Not an octal number: \"\"");
+ }
+
+ parse_nat::<8>(raw.as_str())
+}
+
+#[builtin]
+pub fn builtin_parse_hex(raw: IStr) -> Result<f64> {
+ if raw.is_empty() {
+ throw!("Not hexadecimal: \"\"");
+ }
+
+ parse_nat::<16>(raw.as_str())
+}
+
+fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {
+ debug_assert!(
+ 1 <= BASE && BASE <= 16,
+ "integer base should be between 1 and 16"
+ );
+
+ const ZERO_CODE: u32 = '0' as u32;
+ const UPPER_A_CODE: u32 = 'A' as u32;
+ const LOWER_A_CODE: u32 = 'a' as u32;
+
+ #[inline]
+ fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {
+ if condition {
+ lhs.checked_sub(rhs)
+ } else {
+ None
+ }
+ }
+
+ let base = BASE as f64;
+
+ raw.chars().try_fold(0f64, |aggregate, digit| {
+ let digit = digit as u32;
+ let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {
+ digit + 10
+ } else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {
+ digit + 10
+ } else {
+ digit.checked_sub(ZERO_CODE).unwrap_or(BASE)
+ };
+
+ if digit < BASE {
+ Ok(base * aggregate + digit as f64)
+ } else {
+ throw!("{raw} is not a base {BASE} integer",);
+ }
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_nat_base_10() {
+ assert_eq!(parse_nat::<10>("0").unwrap(), 0.);
+ assert_eq!(parse_nat::<10>("3").unwrap(), 3.);
+ assert_eq!(parse_nat::<10>("27").unwrap(), 10. * 2. + 7.);
+ assert_eq!(parse_nat::<10>("123").unwrap(), 10. * (10. * 1. + 2.) + 3.);
+ }
+}