You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .github/design.md
+88-16Lines changed: 88 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -615,19 +615,20 @@ Using `IEnumerable<JsonElement>` with `yield return` matches jq's semantics natu
615
615
616
616
---
617
617
618
-
## 14. Module System (`include`, `import`)
618
+
## 14. Module System (`include`, `import`, `module`, `modulemeta`)
619
619
620
620
### 14.1 Overview
621
621
622
-
JQSharp supports three module/data-loading forms:
622
+
JQSharp supports four module/data-loading forms:
623
623
624
624
| Form | Purpose | Scope behavior |
625
625
|------|---------|----------------|
626
626
|`include "relative/path" [<metadata>];`| Inline a jq module at parse time | Definitions become directly visible (unqualified names). |
627
627
|`import "relative/path" as alias [<metadata>];`| Import jq `def` declarations under a namespace alias | Definitions are exposed as `alias::name`. |
628
628
|`import "relative/path" as $alias [<metadata>];`| Import JSON data as a variable binding | Data is bound as `$alias::alias`. |
629
+
|`module <metadata>;`| Attach declarative metadata to module file | No direct evaluation effect; consumed by `modulemeta`. |
629
630
630
-
`include` performs content splicing into the current parse stream. `import` does not splice caller text: it either registers alias-prefixed function definitions (module import) or introduces a scoped variable binding (data import).
631
+
`include` performs content splicing into the current parse stream. `import` does not splice caller text: it either registers alias-prefixed function definitions (module import) or introduces a scoped variable binding (data import).`module` is a top-of-module declarative statement.
631
632
632
633
### 14.2 JqResolver
633
634
@@ -651,7 +652,30 @@ Two built-in implementations are provided:
651
652
|`JqFileResolver`| Resolves paths on the file system. Appends `.jq` when no extension is present. Relative paths are resolved from the including/importing module's directory (or `BaseDirectory` for top-level calls). |
652
653
|`JqResourceResolver`| Resolves paths to embedded assembly resources. Slashes are converted to dots; a configurable `Prefix` is prepended; supports nested relative include/import chains. `GetCanonicalPath` checks the **original input path** for `.jq`/`.json` before dot-normalization, and appends `.jq` only when neither extension exists. |
653
654
654
-
### 14.3 Include Parse-Time Content Splicing
655
+
### 14.3 Module Metadata Registry and Statement
656
+
657
+
The parser now maintains a shared metadata registry across nested parser instances:
1.`ParsePrimary()` recognizes `module` keyword and dispatches `ParseModuleStatement()`.
674
+
2. The metadata expression is parsed as normal jq expression and immediately evaluated against `null` input with `JqEnvironment.Empty`.
675
+
3. Result must be a single object value; it is stored as `_moduleStatementMetadata`.
676
+
4. Statement consumes `;` and parsing continues with `ParsePipe()`.
677
+
678
+
### 14.4 Include Parse-Time Content Splicing
655
679
656
680
`include` is handled entirely at parse time using a **content-splice** strategy:
657
681
@@ -669,15 +693,16 @@ rest_of_program
669
693
When `ParseIncludeExpression()` is invoked:
670
694
671
695
1. The module path string is parsed (no interpolation allowed).
672
-
2. An optional metadata object `{...}` is skipped (parsed but currently ignored).
696
+
2. An optional metadata object `{...}` is parsed and preserved for dependency metadata.
673
697
3. The terminating `;` is consumed.
674
698
4.`GetCanonicalPath()` produces the cache key; if cached, file/resource IO is skipped.
675
-
5. The parser captures the remaining caller text (`text[position..]`).
676
-
6. Combined text is built as `moduleContent + "\n" + remainingText`.
677
-
7.`ParseSubExpression(combinedText, canonicalPath)` parses the combined stream in a child parser.
678
-
8. The parent advances `position` to `text.Length` because continuation is consumed by the child.
699
+
5. The module is parsed in isolation first to populate metadata registry (`module` keys, `deps`, `defs`).
700
+
6. The parser captures the remaining caller text (`text[position..]`).
701
+
7. Combined text is built as `moduleContent + "\n" + remainingText`.
702
+
8.`ParseSubExpression(combinedText, canonicalPath)` parses the combined stream in a child parser.
703
+
9. The parent advances `position` to `text.Length` because continuation is consumed by the child.
679
704
680
-
### 14.4 Import Parse Pipeline (Phase 16.2)
705
+
### 14.5 Import Parse Pipeline (Phase 16.3)
681
706
682
707
`import` first parses the shared prefix, then dispatches:
683
708
@@ -700,7 +725,7 @@ flowchart TD
700
725
F --> G
701
726
```
702
727
703
-
### 14.5 Function Import: Parse-Time Definition Extraction
728
+
### 14.6 Function Import: Parse-Time Definition Extraction
704
729
705
730
Function import extracts only exported `def` declarations from the child parse and re-registers them in the parent under `alias::` names.
706
731
@@ -735,7 +760,7 @@ finally
735
760
}
736
761
```
737
762
738
-
### 14.6 Data Import: Variable Binding
763
+
### 14.7 Data Import: Variable Binding
739
764
740
765
Data import (`import "data" as $cfg;`) loads JSON and binds it as a scoped variable named `$cfg::cfg`.
741
766
@@ -749,7 +774,7 @@ Data import (`import "data" as $cfg;`) loads JSON and binds it as a scoped varia
749
774
5. If JSON parsing fails, `JsonException` is wrapped as:
750
775
-`JqException("Failed to parse JSON data from '...': ...")`
751
776
752
-
### 14.7 Qualified Name Resolution (`::`)
777
+
### 14.8 Qualified Name Resolution (`::`)
753
778
754
779
`ParsePrimary()` supports `::` for both variable and identifier/function references.
755
780
@@ -760,11 +785,58 @@ Data import (`import "data" as $cfg;`) loads JSON and binds it as a scoped varia
760
785
761
786
This ensures module-qualified symbols are explicit and never accidentally treated as builtins or filter parameters.
762
787
763
-
### 14.8 Caching
788
+
### 14.9 Metadata Object Shape (`modulemeta`)
789
+
790
+
For each imported/included module, registry stores a metadata object:
791
+
792
+
```json
793
+
{
794
+
"...custom-module-keys": "...",
795
+
"deps": [
796
+
{
797
+
"relpath": "path",
798
+
"as": "alias-or-null",
799
+
"is_data": false,
800
+
"...import/include-metadata-keys": "..."
801
+
}
802
+
],
803
+
"defs": ["name/arity"]
804
+
}
805
+
```
806
+
807
+
Notes:
808
+
809
+
-`deps` entries are recorded for `include`, function `import`, and data `import`.
810
+
-`as` is `null` for `include`.
811
+
-`is_data` is `true` only for `import ... as $alias`.
812
+
-`defs` is collected from exported defs discovered when parsing module in import/metadata-collection mode.
813
+
814
+
### 14.10 Runtime Environment Injection
815
+
816
+
`JqEnvironment` now carries immutable module metadata map. At parse entrypoint, when metadata registry is non-empty, parsed filter is wrapped in `ModuleMetadataFilter` which injects metadata into the evaluation environment.
817
+
818
+
### 14.11 `modulemeta` builtin
819
+
820
+
`modulemeta` is a zero-arg builtin that consumes input as module name:
821
+
822
+
```jq
823
+
"foo" | modulemeta
824
+
```
825
+
826
+
Behavior:
827
+
828
+
- Input must be string, else error: `modulemeta input must be a string`
829
+
- Unknown module name errors: `Unknown module: 'foo'`
830
+
- Returns metadata object with custom keys plus `deps` and `defs`
831
+
832
+
### 14.12 Caching
833
+
834
+
The parser now uses two shared dictionaries across nested parse operations in one `Jq.Parse()` call:
764
835
765
-
The parser holds `Dictionary<string, string> _moduleCache` (keyed by canonical path), shared across nested include/import operations in a single `Jq.Parse()` call. Repeated loads of the same canonical path perform only one resolver read.
0 commit comments