1 package io.jawk.jrt;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * Jawk
6 * ჻჻჻჻჻჻
7 * Copyright (C) 2006 - 2026 MetricsHub
8 * ჻჻჻჻჻჻
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation, either version 3 of the
12 * License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Lesser Public License for more details.
18 *
19 * You should have received a copy of the GNU General Lesser Public
20 * License along with this program. If not, see
21 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23 */
24
25 import java.util.List;
26 import java.util.Map;
27 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28 import io.jawk.intermediate.UninitializedObject;
29 import io.jawk.intermediate.UntypedObject;
30
31 /**
32 * An AWK associative array.
33 * <p>
34 * This interface extends {@link Map} and provides AWK-specific behaviour:
35 * automatic key normalization (null and uninitialized values map to {@code ""}),
36 * numeric key coercion ({@code "1"} and {@code 1L} address the same slot), and
37 * auto-creation of blank entries on first access.
38 * </p>
39 * <p>
40 * Concrete implementations directly extend a JDK {@link Map} class to avoid
41 * delegation overhead:
42 * </p>
43 * <ul>
44 * <li>{@link HashAssocArray} — backed by {@link java.util.HashMap}</li>
45 * <li>{@link ListAssocArray} — materialized from a {@link java.util.List}
46 * and backed by {@link java.util.HashMap}</li>
47 * <li>{@link SortedAssocArray} — backed by {@link java.util.TreeMap} with
48 * AWK key ordering</li>
49 * </ul>
50 * <p>
51 * Use the factory methods to create instances:
52 * </p>
53 *
54 * <pre>
55 * AssocArray hash = AssocArray.createHash();
56 * AssocArray sorted = AssocArray.createSorted();
57 * AssocArray list = AssocArray.createFromList(values, sortedArrayKeys);
58 * AssocArray aa = AssocArray.create(sortedArrayKeys);
59 * </pre>
60 *
61 * @author Danny Daglas
62 */
63 public interface AssocArray extends Map<Object, Object> {
64
65 /** A blank (uninitialized) value shared across all AWK array accesses. */
66 UninitializedObject BLANK = new UninitializedObject();
67
68 /** An untyped value shared across newly referenced AWK array elements. */
69 UntypedObject UNTYPED = new UntypedObject();
70
71 // -------------------------------------------------------------------------
72 // Key-normalization helpers (used by concrete implementations)
73 // -------------------------------------------------------------------------
74
75 /**
76 * Converts a key to the canonical form expected by AWK: {@code null} and
77 * {@link UninitializedObject} map to the empty string, and internal input
78 * strings map to their string value.
79 *
80 * @param key the raw key
81 * @return the normalized key, never {@code null}
82 */
83 static Object normalizeKey(Object key) {
84 if (key == null || key instanceof UninitializedObject) {
85 return "";
86 }
87 if (key instanceof StrNum) {
88 return key.toString();
89 }
90 if (key instanceof Double || key instanceof Float) {
91 double numericKey = ((Number) key).doubleValue();
92 if (JRT.isActuallyLong(numericKey)) {
93 return Long.valueOf((long) Math.rint(numericKey));
94 }
95 }
96 return key;
97 }
98
99 /**
100 * Returns whether a subscript value is an integral number that
101 * {@link #normalizeKey(Object)} canonicalizes to the same {@code Long}
102 * key as its AWK string form. Such a subscript can be used as a key
103 * directly, without the CONVFMT string conversion that every other
104 * scalar goes through: the resulting key is identical, and skipping the
105 * string round-trip keeps integer-indexed loops fast.
106 *
107 * @param value the subscript value to examine
108 * @return {@code true} if the value can be used as a key without string
109 * conversion
110 */
111 static boolean isIntegralNumberKey(Object value) {
112 if (value instanceof Long || value instanceof Integer) {
113 return true;
114 }
115 return value instanceof Double && JRT.toScalarNumber((Double) value) instanceof Long;
116 }
117
118 /**
119 * Attempts to parse the key as a {@code Long}.
120 * <p>
121 * This accepts and rejects exactly the same inputs as
122 * {@link Long#parseLong(String)} (radix 10), but scans the string without
123 * ever constructing an exception: most keys are not integer strings (every
124 * multidimensional {@code arr[x,y]} key, for instance), and paying the cost
125 * of a {@link NumberFormatException} stack-trace fill on every array access
126 * is prohibitive.
127 * </p>
128 *
129 * @param key the key to parse (must not be {@code null})
130 * @return the {@code Long} value, or {@code null} if the key cannot be parsed
131 * as a long integer
132 */
133 @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Defensive check against contract-violating toString() implementations returning null")
134 static Long toLongKey(Object key) {
135 final String str;
136 if (key instanceof String) {
137 str = (String) key;
138 } else {
139 try {
140 str = key.toString();
141 } catch (RuntimeException e) { // NOPMD - EmptyCatchBlock: intentionally ignored
142 // e.g. an AssocArray used as a key throws on toString():
143 // treat such keys as non-numeric
144 return null;
145 }
146 if (str == null) {
147 // a contract-violating toString(): treat the key as non-numeric,
148 // like the former Long.parseLong(null) NumberFormatException path
149 return null;
150 }
151 }
152 final int len = str.length();
153 if (len == 0) {
154 return null;
155 }
156 int i = 0;
157 boolean negative = false;
158 long limit = -Long.MAX_VALUE;
159 final char firstChar = str.charAt(0);
160 if (firstChar < '0') { // possible leading "+" or "-"
161 if (firstChar == '-') {
162 negative = true;
163 limit = Long.MIN_VALUE;
164 } else if (firstChar != '+') {
165 return null;
166 }
167 if (len == 1) { // lone "+" or "-"
168 return null;
169 }
170 i = 1;
171 }
172 // Accumulate negatively, exactly like Long.parseLong, so that
173 // Long.MIN_VALUE (whose magnitude exceeds Long.MAX_VALUE) parses too
174 final long multmin = limit / 10;
175 long result = 0;
176 while (i < len) {
177 // Character.digit keeps parity with Long.parseLong on non-ASCII digits
178 final int digit = Character.digit(str.charAt(i++), 10);
179 if (digit < 0 || result < multmin) {
180 return null;
181 }
182 result *= 10;
183 if (result < limit + digit) {
184 return null;
185 }
186 result -= digit;
187 }
188 return negative ? result : -result;
189 }
190
191 // -------------------------------------------------------------------------
192 // AWK-specific default methods
193 // -------------------------------------------------------------------------
194
195 /**
196 * Returns whether a particular key is contained within the associative array.
197 * <p>
198 * Unlike {@link #get(Object)}, which auto-creates a blank entry when the key
199 * is absent, this method does not modify the array. It exists to support the
200 * AWK {@code IN} keyword.
201 * </p>
202 *
203 * @param key Key to be checked
204 * @return {@code true} if the key (or its numeric equivalent) is present
205 */
206 default boolean isIn(Object key) {
207 key = normalizeKey(key);
208 if (containsKey(key)) {
209 return true;
210 }
211 Long lKey = toLongKey(key);
212 return lKey != null && containsKey(lKey);
213 }
214
215 /**
216 * Provides a string representation of this associative array, recursively
217 * rendering nested arrays.
218 *
219 * @return a human-readable map string of the form {@code {key=value, ...}}
220 */
221 default String mapString() {
222 // Since extensions allow assoc arrays to become keys as well,
223 // we render nested arrays recursively rather than using toString().
224 StringBuilder sb = new StringBuilder().append('{');
225 int cnt = 0;
226 for (Map.Entry<Object, Object> entry : entrySet()) {
227 if (cnt > 0) {
228 sb.append(", ");
229 }
230 Object key = entry.getKey();
231 if (key instanceof AssocArray) {
232 sb.append(((AssocArray) key).mapString());
233 } else {
234 sb.append(key.toString());
235 }
236 sb.append('=');
237 Object value = entry.getValue();
238 if (value instanceof AssocArray) {
239 sb.append(((AssocArray) value).mapString());
240 } else {
241 sb.append(value.toString());
242 }
243 ++cnt;
244 }
245 return sb.append('}').toString();
246 }
247
248 /**
249 * Stores a value using a primitive {@code long} key, bypassing string parsing.
250 * <p>
251 * This is a convenience overload for callers that already hold a {@code long}
252 * key. The default implementation boxes the key and delegates to
253 * {@link #put(Object, Object)}.
254 * </p>
255 *
256 * @param key the long key
257 * @param value the value to associate with the key
258 * @return the previous value associated with the key, or {@code null}
259 */
260 default Object put(long key, Object value) {
261 return put(Long.valueOf(key), value);
262 }
263
264 /**
265 * Returns the specification version of the underlying JDK {@link Map} class
266 * that backs this implementation.
267 *
268 * @return the specification version string, or {@code null} if unavailable
269 */
270 default String getMapVersion() {
271 return getClass().getSuperclass().getPackage().getSpecificationVersion();
272 }
273
274 // -------------------------------------------------------------------------
275 // Factory methods
276 // -------------------------------------------------------------------------
277
278 /**
279 * Creates a new hash-based associative array (backed by {@link java.util.HashMap}).
280 *
281 * @return a new {@link HashAssocArray}
282 */
283 static AssocArray createHash() {
284 return new HashAssocArray();
285 }
286
287 /**
288 * Creates a new sorted associative array (backed by {@link java.util.TreeMap}
289 * with AWK key ordering).
290 *
291 * @return a new {@link SortedAssocArray}
292 */
293 static AssocArray createSorted() {
294 return new SortedAssocArray();
295 }
296
297 /**
298 * Creates a new associative array of the appropriate type.
299 *
300 * @param sortedArrayKeys {@code true} to create a sorted (tree-backed) array,
301 * {@code false} for a hash-backed array
302 * @return a new {@link AssocArray} instance
303 */
304 static AssocArray create(boolean sortedArrayKeys) {
305 return sortedArrayKeys ? createSorted() : createHash();
306 }
307
308 /**
309 * Creates a new associative array materialized from a Java {@link List}.
310 * <p>
311 * List elements are stored under zero-based {@link Long} keys. Nested
312 * {@link List} values are recursively materialized as associative arrays so
313 * JSON-like object trees can be traversed with AWK array syntax.
314 * </p>
315 *
316 * @param values list values to expose as an AWK array
317 * @param sortedArrayKeys {@code true} to create a sorted array, {@code false}
318 * for a hash-backed array
319 * @return a new {@link AssocArray} containing the list values
320 */
321 static AssocArray createFromList(List<?> values, boolean sortedArrayKeys) {
322 return ListAssocArray.createFromList(values, sortedArrayKeys);
323 }
324
325 /**
326 * Normalizes an externally supplied structured value before the AVM stores it.
327 * <p>
328 * {@link List} values are converted to {@link AssocArray} instances.
329 * {@link Map} values are kept in place to preserve direct-map performance, but
330 * their nested list values are recursively converted.
331 * </p>
332 *
333 * @param value value to normalize
334 * @param sortedArrayKeys {@code true} when converted lists should use sorted
335 * array keys
336 * @return the normalized value
337 */
338 static Object normalizeValue(Object value, boolean sortedArrayKeys) {
339 return ListAssocArray.normalizeValue(value, sortedArrayKeys);
340 }
341
342 }