GlobalFunctionAccessor.vala 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. using Invercargill.DataStructures;
  2. namespace Invercargill.Expressions {
  3. /**
  4. * Function accessor for implicitly available global functions.
  5. *
  6. * These functions are always available in expressions without needing
  7. * to be explicitly registered. Currently supports:
  8. *
  9. * - `format(template, ...args)` - Printf-style string formatting
  10. *
  11. * Examples:
  12. * - `format("Hello, %s!", name)` - String substitution
  13. * - `format("You have %i items", count)` - Integer substitution
  14. * - `format("Value: %.2f", price)` - Float formatting with precision
  15. * - `format("%s #%i: %s", category, id, title)` - Multiple arguments
  16. */
  17. public class GlobalFunctionAccessor : Object, FunctionAccessor {
  18. private static Series<string>? _function_names = null;
  19. /**
  20. * Gets the names of all available global functions.
  21. */
  22. public Enumerable<string> get_function_names() {
  23. if (_function_names == null) {
  24. _function_names = new Series<string>();
  25. _function_names.add("format");
  26. }
  27. return _function_names;
  28. }
  29. /**
  30. * Checks if a global function exists.
  31. */
  32. public bool has_function(string name) {
  33. return name == "format";
  34. }
  35. /**
  36. * Calls a global function with the given arguments.
  37. *
  38. * @param function_name The function name
  39. * @param arguments The function arguments as a Series
  40. * @param context The evaluation context
  41. * @return The result of the function call
  42. * @throws ExpressionError if the function doesn't exist or arguments are invalid
  43. */
  44. public Element call_function(string function_name, Series<Element> arguments, EvaluationContext context) throws ExpressionError {
  45. if (function_name == "format") {
  46. return call_format(arguments);
  47. }
  48. throw new ExpressionError.FUNCTION_NOT_FOUND(
  49. @"Unknown global function '$function_name'"
  50. );
  51. }
  52. /**
  53. * Calls the format function.
  54. *
  55. * Signature: format(template: string, ...args: any) -> string
  56. *
  57. * Applies printf-style formatting to the template string using
  58. * the provided arguments. Supports:
  59. * - %s - String
  60. * - %i, %d - Integer
  61. * - %f - Float/double
  62. * - %x - Hexadecimal (lowercase)
  63. * - %X - Hexadecimal (uppercase)
  64. * - %o - Octal
  65. * - %% - Literal percent sign
  66. *
  67. * Format specifiers can include width and precision, e.g.:
  68. * - %10s - Right-aligned string in 10 characters
  69. * - %-10s - Left-aligned string in 10 characters
  70. * - %.2f - Float with 2 decimal places
  71. * - %05i - Integer padded with zeros to 5 digits
  72. */
  73. private Element call_format(Series<Element> arguments) throws ExpressionError {
  74. var args = arguments.to_array();
  75. if (args.length == 0) {
  76. throw new ExpressionError.INVALID_ARGUMENTS(
  77. "format() requires at least 1 argument (the format template)"
  78. );
  79. }
  80. // Get the format template
  81. string? template;
  82. if (!args[0].try_get_as<string?>(out template)) {
  83. throw new ExpressionError.TYPE_MISMATCH(
  84. "format() first argument must be a string template"
  85. );
  86. }
  87. if (template == null) {
  88. return new NativeElement<string?>((string?)null);
  89. }
  90. // Build the formatted string by processing format specifiers
  91. // (even with no args, we need to handle %% escape sequences)
  92. var result = new StringBuilder();
  93. int arg_index = 1;
  94. int i = 0;
  95. while (i < template.length) {
  96. if (template[i] == '%' && i + 1 < template.length) {
  97. // Parse format specifier
  98. int start = i;
  99. i++; // Skip '%'
  100. // Handle literal %
  101. if (template[i] == '%') {
  102. result.append_c('%');
  103. i++;
  104. continue;
  105. }
  106. // Parse flags, width, precision
  107. var spec = new StringBuilder();
  108. spec.append_c('%');
  109. // Flags: -, +, space, #, 0
  110. while (i < template.length && (template[i] == '-' || template[i] == '+' ||
  111. template[i] == ' ' || template[i] == '#' || template[i] == '0')) {
  112. spec.append_c(template[i]);
  113. i++;
  114. }
  115. // Width
  116. while (i < template.length && template[i].isdigit()) {
  117. spec.append_c(template[i]);
  118. i++;
  119. }
  120. // Precision
  121. if (i < template.length && template[i] == '.') {
  122. spec.append_c(template[i]);
  123. i++;
  124. while (i < template.length && template[i].isdigit()) {
  125. spec.append_c(template[i]);
  126. i++;
  127. }
  128. }
  129. // Length modifier (l, ll, h, etc.) - skip for now
  130. while (i < template.length && (template[i] == 'l' || template[i] == 'h' ||
  131. template[i] == 'L' || template[i] == 'z' || template[i] == 'j')) {
  132. spec.append_c(template[i]);
  133. i++;
  134. }
  135. // Conversion specifier
  136. if (i < template.length) {
  137. char conv = template[i];
  138. spec.append_c(conv);
  139. i++;
  140. // Get the argument
  141. if (arg_index >= args.length) {
  142. throw new ExpressionError.INVALID_ARGUMENTS(
  143. @"format() has more format specifiers than arguments"
  144. );
  145. }
  146. var arg = args[arg_index];
  147. arg_index++;
  148. // Format based on conversion specifier
  149. string? formatted = format_arg(spec.str, conv, arg);
  150. if (formatted != null) {
  151. result.append(formatted);
  152. }
  153. }
  154. } else {
  155. result.append_c(template[i]);
  156. i++;
  157. }
  158. }
  159. return new NativeElement<string>(result.str);
  160. }
  161. /**
  162. * Formats a single argument using the format specifier.
  163. */
  164. private string? format_arg(string spec, char conv, Element arg) throws ExpressionError {
  165. switch (conv) {
  166. case 's': // String
  167. string? str_val;
  168. if (arg.try_get_as<string?>(out str_val)) {
  169. return spec.printf(str_val ?? "(null)");
  170. }
  171. // Convert to string representation
  172. return spec.printf(arg.to_string());
  173. case 'd':
  174. case 'i': // Integer
  175. // Try int64 first (common in expression system)
  176. int64? int64_val;
  177. if (arg.try_get_as<int64?>(out int64_val) && int64_val != null) {
  178. return "%lli".printf((int64)int64_val);
  179. }
  180. int? int_val;
  181. if (arg.try_get_as<int?>(out int_val) && int_val != null) {
  182. return "%i".printf((int)int_val);
  183. }
  184. long? long_val;
  185. if (arg.try_get_as<long?>(out long_val) && long_val != null) {
  186. return "%li".printf((long)long_val);
  187. }
  188. // Try to convert double to int
  189. double? dbl_val;
  190. if (arg.try_get_as<double?>(out dbl_val) && dbl_val != null) {
  191. return "%i".printf((int)(double)dbl_val);
  192. }
  193. throw new ExpressionError.TYPE_MISMATCH(
  194. @"format() specifier '%%i' requires an integer, got $(arg.type_name())"
  195. );
  196. case 'f':
  197. case 'e':
  198. case 'E':
  199. case 'g':
  200. case 'G': // Float/double
  201. double? dbl_val;
  202. if (arg.try_get_as<double?>(out dbl_val) && dbl_val != null) {
  203. return spec.printf((double)dbl_val);
  204. }
  205. // Try integer as double
  206. int? int_val;
  207. if (arg.try_get_as<int?>(out int_val) && int_val != null) {
  208. return spec.printf((double)int_val);
  209. }
  210. int64? int64_val;
  211. if (arg.try_get_as<int64?>(out int64_val) && int64_val != null) {
  212. return spec.printf((double)int64_val);
  213. }
  214. throw new ExpressionError.TYPE_MISMATCH(
  215. @"format() specifier '%%f' requires a number, got $(arg.type_name())"
  216. );
  217. case 'x':
  218. case 'X': // Hexadecimal
  219. int64? hex_int64;
  220. if (arg.try_get_as<int64?>(out hex_int64) && hex_int64 != null) {
  221. if (conv == 'x') {
  222. return "%llx".printf((int64)hex_int64);
  223. } else {
  224. return "%llX".printf((int64)hex_int64);
  225. }
  226. }
  227. int? hex_int;
  228. if (arg.try_get_as<int?>(out hex_int) && hex_int != null) {
  229. if (conv == 'x') {
  230. return "%x".printf((int)hex_int);
  231. } else {
  232. return "%X".printf((int)hex_int);
  233. }
  234. }
  235. long? hex_long;
  236. if (arg.try_get_as<long?>(out hex_long) && hex_long != null) {
  237. if (conv == 'x') {
  238. return "%lx".printf((long)hex_long);
  239. } else {
  240. return "%lX".printf((long)hex_long);
  241. }
  242. }
  243. throw new ExpressionError.TYPE_MISMATCH(
  244. @"format() specifier '%%x' requires an integer, got $(arg.type_name())"
  245. );
  246. case 'o': // Octal
  247. int64? oct_int64;
  248. if (arg.try_get_as<int64?>(out oct_int64) && oct_int64 != null) {
  249. return "%llo".printf((int64)oct_int64);
  250. }
  251. int? oct_int;
  252. if (arg.try_get_as<int?>(out oct_int) && oct_int != null) {
  253. return "%o".printf((int)oct_int);
  254. }
  255. long? oct_long;
  256. if (arg.try_get_as<long?>(out oct_long) && oct_long != null) {
  257. return "%lo".printf((long)oct_long);
  258. }
  259. throw new ExpressionError.TYPE_MISMATCH(
  260. @"format() specifier '%%o' requires an integer, got $(arg.type_name())"
  261. );
  262. case 'c': // Character
  263. int char_int;
  264. if (arg.try_get_as<int>(out char_int)) {
  265. return spec.printf(char_int);
  266. }
  267. string? char_str;
  268. if (arg.try_get_as<string?>(out char_str) && char_str != null && char_str.length > 0) {
  269. return spec.printf(char_str[0]);
  270. }
  271. throw new ExpressionError.TYPE_MISMATCH(
  272. "format() specifier '%%c' requires an integer or single-character string"
  273. );
  274. case 'p': // Pointer (not really useful, but supported)
  275. return spec.printf((void*)null);
  276. default:
  277. throw new ExpressionError.INVALID_ARGUMENTS(
  278. @"format() unsupported format specifier '%%$conv'"
  279. );
  280. }
  281. }
  282. }
  283. }