Skip to content

Implement gawk array sorting and type introspection builtins#504

Open
bertysentry wants to merge 8 commits into
mainfrom
454-implement-gawk-array-sorting-and-type-introspection-builtins
Open

Implement gawk array sorting and type introspection builtins#504
bertysentry wants to merge 8 commits into
mainfrom
454-implement-gawk-array-sorting-and-type-introspection-builtins

Conversation

@bertysentry

Copy link
Copy Markdown
Contributor

Closes #454

What changed

GawkExtension, enabled by default (in new Awk() and the CLI when no -l is given), implementing:

  • asort(source [, dest [, how]]) / asorti(...) — gawk array sorting with the predefined comparison modes (@ind_str_asc, @val_num_desc, @val_type_asc, @unsorted, …), honoring IGNORECASE
  • typeof(x) — gawk type categories (number, string, strnum, array, regexp, number|bool, unassigned, untyped)
  • isarray(x), mkbool(x), and gensub(re, repl, how [, target]) (gawk semantics: any g/G-prefixed selector is global, \N backreferences)
  • PROCINFO["sorted_in"]-controlled for (i in a) traversal, re-read at each loop so mid-execution changes apply
  • Honest SYMTAB (script globals + Jawk special variables, snapshot) and FUNCTAB (user-defined functions + extension keywords) for scripts that reference them

Extension SPI — gawk-specific behavior lives entirely in the extension; the core gained neutral hooks:

  • @JawkBeforeStart — setup method run after globals are allocated, before execution
  • @JawkRawValue — parameter receives the runtime value uncoerced (how typeof/isarray see untyped state); backed by new PEEK_DEREFERENCE / PEEK_ARRAY_ELEMENT_RAW opcodes that observe without autovivifying
  • @JawkRegexp — parameter keeps regexp literals as precompiled patterns
  • Method-level @JawkAssocArray({...}) for array positions consumed through varargs
  • ForInKeyOrder — extension-provided for-in traversal order; the default path is unchanged (single keySet() copy, null-check only)

Interpreter fixes riding along:

  • Regexp literal as an ordinary expression now evaluates as $0 ~ /re/ per POSIX (x = /re/ yields 0/1)
  • Typed regexp literals (@/re/) accepted; rejected under --posix
  • Calling a user function with more arguments than declared is a gawk-style runtime warning instead of a compile error; extra expressions are evaluated for side effects then discarded (bare variables are peeked, preserving untyped state)
  • Runtime warnings go to a dedicated stderr-backed stream — never into the script output an embedding host captures (CLI routes it to its stderr; errorStream(...) captures it in the Java API)
  • AVM.getVariable(name) falls back to -v/API-supplied initial variables, so e.g. -v IGNORECASE=1 is honored even when the script never references it
  • sub()/gsub() replacement escaping is shared with gensub via RegexRuntimeSupport

Reviewer notes

  • Extension state is per-engine: the default set instantiates a fresh GawkExtension per Awk; the singleton INSTANCE pattern was removed because AbstractExtension holds per-engine vm/jrt bindings.
  • Serialization: annotation-derived metadata in ExtensionFunction is transient and recomputed from the resolved Method on deserialization, so .tuples files from other versions stay loadable. VariableManager.getVariable(String) is a default method to keep third-party implementors source- and binary-compatible.
  • One intentional test flip: GawkExtensionIT.test_asortsymtab goes from pass to failure. Its .ok fixture only encodes the entry counts of the gawk build's own symbol table (30 globals / 42 builtins); the previous implementation faked those counts with placeholder entries, which corrupted any script actually reading SYMTAB/FUNCTAB. The honest content can't match gawk's internal table sizes.
  • Behavior changes are documented in index.md.vm (gawk departures list), extensions.md, extensions-writing.md, cli.md, cli-reference.md, and java.md — including that gensub/typeof/asort/… become reserved names by default.

Verification

  • mvn verify: 479 unit tests, 0 failures; checkstyle, PMD, SpotBugs clean; mvn compile -P benchmark passes
  • Gawk compatibility vs main baseline: 29 cases error→pass (incl. all 13 representative cases from Implement gawk array sorting and type-introspection builtins #454), 10 error→failure (now run, output differs — follow-up candidates), 1 pass→failure (asortsymtab, intentional, see above), zero other regressions across POSIX/BWK/gawk suites

🤖 Generated with Claude Code

Add GawkExtension, enabled by default, providing asort(), asorti(),
typeof(), isarray(), mkbool(), and gensub(), with runtime support for
PROCINFO["sorted_in"] traversal order, IGNORECASE-aware sorting, and
gawk's typed/untyped value distinctions.

Extension SPI additions: @JawkBeforeStart startup hook, @JawkRawValue
and @JawkRegexp parameter annotations, method-level @JawkAssocArray
positions, and a ForInKeyOrder hook so extensions control for-in
traversal without gawk-specific code in AVM/JRT/AwkParser.

Also fix regexp literal expression semantics (/re/ evaluates as
$0 ~ /re/ per POSIX), accept extra user-function arguments with a
gawk-style runtime warning (evaluated then discarded), route runtime
warnings to stderr instead of the output sink, expose honest
SYMTAB/FUNCTAB content, and keep extension state per-engine.

Closes #454

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry bertysentry linked an issue Jul 7, 2026 that may be closed by this pull request

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a8e20523b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/io/jawk/frontend/AwkParser.java Outdated
Comment thread src/main/java/io/jawk/Awk.java Outdated
Comment on lines +219 to +220
if (extensions == null || extensions.isEmpty()) {
return ExtensionSetup.EMPTY;
return ExtensionSetup.createDefault();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit empty extension lists

When a Java caller passes an explicit empty extension list, this treats it the same as omitting extensions and installs GawkExtension anyway. That leaves no public Java API path for hosts to run with no extensions, so scripts that intentionally need to reclaim names such as gensub or typeof still see those names reserved even with new Awk(Collections.emptyList()) or an empty varargs call.

Useful? React with 👍 / 👎.

bertysentry and others added 2 commits July 8, 2026 12:44
typeof()/isarray() on a missing array element now bring that element
into existence (observable via `in`, delete, for-in), matching gawk.
Achieved by reading array-element raw values through the normal
element dereference, which already returns the untyped marker while
creating the element; the special PEEK_ARRAY_ELEMENT_RAW opcode and
JRT.peekAwkValue are removed as no longer needed.

An explicit empty extension list (new Awk(emptyList) or empty varargs)
is now honored as "no extensions" instead of silently installing the
default set, giving Java hosts a way to reclaim names such as gensub
or typeof. The no-argument constructors still install the default set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tics

Extension SPI:
- Add @JawkOptional for trailing optional parameters (null when omitted);
  asort/asorti/typeof/gensub now declare real signatures instead of
  varargs. @JawkAssocArray is parameter-level only again: the method-level
  indexed form is gone (indexes were unfriendly), and a pure-runtime Map
  check cannot work because a fresh dest variable must be compiled as an
  array reference.
- Make StrNum public: it is the strnum type class and extensions need it.

Diagnostics:
- Replace the per-tuple runtimeWarning fields (CallFunctionTuple,
  ExtensionTuple) with a single WARNING opcode the parser plants before
  the instruction it describes.
- Move the gensub third-argument warning out of the parser into
  GawkExtension itself, where it now checks the actual runtime value
  (variable selectors warn too, like gawk); AVM exposes the source
  description and current call line for extension diagnostics, and
  extension call sites are stamped with the call-site line.

Core cleanups:
- Rename AwkSettings.setAllowArraysOfArrays to setPosix (logic reversed):
  the flag now gates all POSIX compile-time behavior, including typed
  regexp literals.
- Rename JRT.getAwkValue to getAssocArrayValue.
- Single source of truth for special variable names: AVM exposes
  getSpecialVariableNames() backed by the same map getVariable() uses;
  GawkExtension and execMatch consume it.
- HashAssocArray/SortedAssocArray normalize null values to the untyped
  marker so callers never special-case null.
- VariableManager.getVariable is a plain abstract method (implemented in
  the JMH benchmark); sourceBasename uses File.getName; warnings use
  String.format; comparator helpers documented, including why they cannot
  reuse JRT.compare2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex Please review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a92bcbfc10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/io/jawk/ext/ExtensionRegistry.java Outdated
Comment thread src/main/java/io/jawk/ext/GawkExtension.java Outdated
ExtensionRegistry now stores factories instead of instances: resolve()
and listExtensions() return a fresh instance for factory-registered
extensions, so the stateful built-ins (GawkExtension, StdinExtension)
are never shared across engines by programmatic callers. A new
register(String, Supplier) overload lets third parties register
factories for their own stateful extensions; register(String, instance)
keeps its sharing semantics. The CLI's GawkExtension clone is obsolete.

SYMTAB now observes initialized runtime globals: the SET_NUM_GLOBALS
handler consumes the runtime-managed preamble tuples (ENVIRON, ARGC,
ARGV offsets) before running the beforeStart hooks, so the gawk
extension's SYMTAB snapshot sees their real values instead of untyped
placeholders. The tuple stream stays the source of truth, which keeps
sandboxed programs (which omit the ARGV tuple on purpose) intact.
ARGC/ARGV also join AVM's special-variable map, answered through their
lazy synthetic accessors, so SYMTAB["ARGV"][0] works in embedded runs
where ARGV is never materialized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex Please review again.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6143c74242

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* Stack before: ...<br/>
* Stack after: x ...
*/
PEEK_DEREFERENCE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve eval frames for raw dereferences

When an AwkExpression contains a raw-value extension call such as typeof(x) or isarray(x), the tuple stream uses this new PEEK_DEREFERENCE opcode instead of DEREFERENCE. The eval optimizer only keeps the SET_NUM_GLOBALS initializer for opcodes listed in requiresEvalGlobalFrame, and this opcode is not listed there, so it can remove the global-frame setup; executing the peek then reads runtimeStack.globals while it is still null. Mark this opcode as requiring an eval global frame, like DEREFERENCE, so eval of raw global variables does not fail before the extension runs.

Useful? React with 👍 / 👎.

key = lKey;
}
result = BLANK;
result = UNTYPED;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize untyped array reads before field assignment

Returning UNTYPED from a normal missing-element read leaks a subclass of UninitializedObject into the existing field-assignment path. With default arrays, BEGIN { $3 = a[1]; print NF } now goes down jrtSetInputField's uninitialized branch and does not create fields 1..3, whereas the previous blank value took the normal assignment path; the sorted-array implementation has the same change. Convert this marker before ordinary scalar/field assignment, or otherwise keep it out of non-introspection reads.

Useful? React with 👍 / 👎.

Comment on lines +249 to +250
Pattern pattern = regexp instanceof Pattern ?
(Pattern) regexp : Pattern.compile(toAwkString(regexp));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor IGNORECASE in gensub matches

When IGNORECASE is nonzero, gawk makes regexp operations case-insensitive (GNU awk manual), but gensub matches with the precompiled literal Pattern or a freshly compiled string pattern that has no CASE_INSENSITIVE flag. For example, after IGNORECASE = 1, gensub(/a/, "x", "g", "A") still leaves A unchanged. Rebuild or compile the pattern with the current IGNORECASE setting before creating the matcher.

Useful? React with 👍 / 👎.

bertysentry and others added 4 commits July 9, 2026 01:11
Add PEEK_DEREFERENCE and EXTENSION to the opcodes requiring the eval
global frame: an AwkExpression containing a raw-value extension call
such as typeof(x) had its SET_NUM_GLOBALS initializer optimized away,
crashing on the null globals array. Extension calls also read globals
(e.g. IGNORECASE) and run beforeStart hooks, so they keep the frame too.

gensub() now honors IGNORECASE: when it is nonzero, the pattern is
recompiled with CASE_INSENSITIVE, matching gawk's rule that IGNORECASE
affects all regexp operations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ENVIRON and ARGV preamble tuples are now emitted only when the script
references them, or when it references SYMTAB (whose snapshot exposes
them); unreferenced ones are answered by the synthetic accessors, so
plain scripts no longer pay for building the environment map. ARGC
stays always materialized: it is a single cheap assignment and its slot
must remain authoritative for ARGC=n command-line operand assignments
to affect input traversal. ARGV population derives its count from the
argument list so it no longer depends on the ARGC slot.

IGNORECASE is now a JRT-managed special variable like FS or RS: reads
and writes compile to dedicated PUSH_IGNORECASE/ASSIGN_IGNORECASE
opcodes, the JRT precomputes the boolean on assignment, and every
regexp/sorting consumer tests jrt.isIgnoreCase() instead of coercing
the value per match. -v/API assignments flow through the same setter.

Wiring IGNORECASE surfaced a general parser hole: ++/-- on JRT-managed
specials compiled to slot-based INC/DEC opcodes that never touched the
JRT (gawk's aasort fixture loops on IGNORECASE++ and span forever).
Pre/post increment and decrement of specials now compile to a
read/adjust/assign sequence through the special-variable opcodes, with
correct prefix/postfix value semantics. The duplicated special-variable
push/assign switches are consolidated into single parser-level methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SYMTAB and FUNCTAB move from GawkExtension into the interpreter, where
they belong: the parser emits UPDATE_SYMTAB/UPDATE_FUNCTAB preamble
tuples only when the script references them outside POSIX mode, and the
AVM populates them from its own metadata (script globals, JRT-managed
specials, host-supplied variables, function names, extension keywords).
This fixes gawk parity for the reported case: -v variables without a
compiled slot now appear in SYMTAB, and command-line name=value operand
assignments update the array live between input files.

The extension beforeStart hooks now run from a dedicated
BEFORE_START_HOOKS opcode emitted by the parser at the end of the
preamble, replacing the AVM's nested preamble-consumption loop, and
execute at most once per AVM: reused interpreters (repeated executions,
expression evaluations) no longer reinitialize their extensions.

The special-variables accessor table is now a static
Map<String, Function<AVM, Object>>, so constructing an AVM allocates
nothing for it. GawkExtension loses all SYMTAB/FUNCTAB code (its
beforeStart hook only registers the for-in ordering), which also
retires the shouldMaterialize/getVariable lookup concern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AVM.getVariable(String) is back to a plain string switch (a compiled
jump table, no map allocation and no lookup indirection); the special
variable names live in a static Set that a unit test keeps in sync with
the switch.

@unsorted now performs no sorting at all: asort()/asorti() skip
Collections.sort entirely (the for-in hook already returned keySet()
directly), and the constant-comparator branch is gone.

JRT.toAssignedScalar is renamed untypedToBlank with documentation of
where the untyped marker comes from (missing-array-element reads) and
why assignments must strip it (x = a[missing] must leave x an assigned
blank scalar, as in gawk). The pointless call on a freshly boxed
Integer in populateArgc is removed; the two real call sites remain,
costing one monomorphic instanceof on the assignment paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement gawk array sorting and type-introspection builtins

1 participant