[ planet-factor ]

John Benediktsson: World Emoji Day

Today, and every July 17th, is World Emoji Day. Kind of amazing that there have already been twelve annual global emoji celebrations! In any event, I was reminded of that by this post:

We have had support in the calendar.holidays vocabulary for defining and computing on holidays, or at least the ones that we have defined already. Turns out, we were missing World Emoji Day!

Well, after adding it, we can now see:

! The last world emoji day (today)
IN: scratchpad today [ world-emoji-day ] year<= .
T{ timestamp
    { year 2025 }
    { month 7 }
    { day 17 }
}

! The next world emoji day!
IN: scratchpad today [ world-emoji-day ] year> .
T{ timestamp
    { year 2026 }
    { month 7 }
    { day 17 }
}

And we can implement the fate checker from the original post by defining our emojis:

CONSTANT: emojis $[
    "๐Ÿ’ฉ๐Ÿคฌ๐Ÿคก๐Ÿ˜ฐ๐Ÿคฎโ˜ ๏ธ๐Ÿ˜ฑ๐Ÿ˜ญ๐Ÿคข๐Ÿ™ƒ๐Ÿ™ˆ๐Ÿ™‰๐Ÿ™Š๐Ÿ‘๐Ÿ‘€๐Ÿ™‚๐Ÿ˜€๐Ÿค—๐Ÿ˜๐Ÿฒ"
    >graphemes [ >string ] map
]

We could choose one at random:

IN: scratchpad emojis random .
"๐Ÿ’ฉ"

Or by rolling dice, and adjusting to zero-based indices:

IN: scratchpad ROLL: 1d20 1 - emojis nth .
"๐Ÿ’ฉ"

Crap.

Thu, 17 Jul 2025 15:00:00

John Benediktsson: One Billion Loops

The Primeagen had a great video about Language Performance Comparisons Are Junk about six months ago talking about the 1 Billion nested loop iterations benchmark that Benjamin Dicken wrote. You can find a copy of the benchmark code on GitHub.

The loops benchmark can be summarized by a version in Python:

import sys
import random

u = int(sys.argv[1])          # Get an input number from the command line
r = random.randint(0, 10000)  # Get a random number 0 <= r < 10k
a = [0] * 10000               # Array of 10k elements initialized to 0
for i in range(10000):        # 10k outer loop iterations
    for j in range(100000):   # 100k inner loop iterations, per outer loop iteration
        a[i] += j % u         # Simple sum
    a[i] += r                 # Add a random value to each element in array
print(a[r])                   # Print out a single element from the array

As a benchmark, it has some flaws, but it was fun to iterate on and I would like to compare Factor as a percentage of Zig – which was the fastest solution – and find out if Factor is faster than Zig! again.

The Zig version takes about 1.3 seconds on the computer I used for benchmarking:

$ git clone https://github.com/bddicken/languages.git

$ cd languages/loops/zig/

$ zig version
0.14.1

$ zig build-exe -O ReleaseFast code.zig

$ time ./code 100
4958365

real    0m1.292s
user    0m1.288s
sys     0m0.002s

Benjamin has some thoughts about using test runners to eliminate startup time and arrive at stable benchmark times. I’m going to ignore this for the purpose of this blog post, but it might be worth revisiting at some point to see what the steady state performance of Factor is.

Version 1

Our first implementation will be the simplest direct copy of the Python version:

:: loops-benchmark ( u -- )
    10,000 random :> r
    10,000 0 <array> :> a
    10,000 [| i |
        100,000 [| j |
            i a [ j u mod + ] change-nth
        ] each-integer
        i a [ r + ] change-nth
    ] each-integer r a nth . ;

This takes 4.7 seconds or about 3.6x of Zig.

Version 2

By default change-nth performs bounds-checking and we can notice that our indices are always within bounds, so we can use the unsafe version instead:

:: loops-benchmark ( u -- )
    10,000 random :> r
    10,000 0 <array> :> a
    10,000 [| i |
        100,000 [| j |
-            i a [ j u mod + ] change-nth
+            i a [ j u mod + ] change-nth-unsafe
        ] each-integer
-        i a [ r + ] change-nth
+        i a [ r + ] change-nth-unsafe
-    ] each-integer r a nth . ;
+    ] each-integer r a nth-unsafe . ;

This does not really speed the benchmark up, taking 4.7 seconds.

Version 3

We can improve our math dispatch by specifying that the argument is an integer:

-:: loops-benchmark ( u -- )
+TYPED:: loops-benchmark ( u: integer -- )
    10,000 random :> r
    10,000 0 <array> :> al
    10,000 [| i |
        100,000 [| j |
            i a [ j u mod + ] change-nth-unsafe
        ] each-integer
        i a [ r + ] change-nth-unsafe
    ] each-integer r a nth-unsafe . ;

This takes 4.5 seconds or about 3.5x of Zig.

Version 4

The Zig version operates on 32-bit unsigned ints, so we could enforce using fixnum integers:

-TYPED:: loops-benchmark ( u: integer -- )
+TYPED:: loops-benchmark ( u: fixnum -- )
-    10,000 random :> r
+    10,000 random { fixnum } declare :> r
    10,000 0 <array> :> a
    10,000 [| i |
        100,000 [| j |
-            i a [ j u mod + ] change-nth-unsafe
+            i a [ j u mod fixnum+fast ] change-nth-unsafe
        ] each-integer
-        i a [ r + ] change-nth-unsafe
+        i a [ r fixnum+fast ] change-nth-unsafe
    ] each-integer r a nth-unsafe . ;

This takes 3.5 seconds or about 2.7x of Zig.

Version 5

We could operate on those same 32-bit unsigned ints using specialized-arrays:

+SPECIALIZED-ARRAY: uint32_t

TYPED:: loops-benchmark ( u: fixnum -- )
    10,000 random { fixnum } declare :> r
-    10,000 0 <array> :> a
+    10,000 uint32_t <c-array> :> a
    10,000 [| i |
        100,000 [| j |
            i a [ j u mod fixnum+fast ] change-nth-unsafe
        ] each-integer
        i a [ r fixnum+fast ] change-nth-unsafe
    ] each-integer r a nth-unsafe . ;

This takes 3.1 seconds or about 2.4x of Zig.

Version 6

We could notice that the value added to the array index can be computed first and then added:

TYPED:: loops-benchmark ( u: fixnum -- )
    10,000 random { fixnum } declare :> r
    10,000 uint32_t <c-array> :> a
    10,000 [| i |
-        100,000 [| j |
-            i a [ j u mod fixnum+fast ] change-nth-unsafe
-        ] each-integer
+        100,000 <iota> [ u mod ] map-sum :> v
+        i a [ v + ] change-nth-unsafe
        i a [ r fixnum+fast ] change-nth-unsafe
    ] each-integer r a nth-unsafe . ;

This takes 2.4 seconds or about 1.8x of Zig.

Version 7

While the previous version did the same number of loops, it did fewer modifications to the array memory. It turns out that not only can the value added to the array index be computed first – it can be computed outside the loop. And once you do that, you’ll notice that each element of the array gets the same value, and you don’t need to compute the array at all. This is effectively a compiler optimization that the compiler isn’t doing and, after writing a little proof in your head, it can reduce down to:

:: loops-benchmark ( u -- )
    10,000 random 100,000 <iota> [ u mod ] map-sum + . ;

This takes 0.0003 seconds which is now 4300x faster than Zig.

Conclusion

I’ve always thought of Factor as able to have about 2x-4x the performance of C with reasonable looking generic and dynamic code. This depends somewhat on which benchmark is being considered, and on occasion we can get as fast as C.

We can easily profile code using the sampling profiler, visualize profiles using the flamegraph vocabulary, print optimized output from the compiler, as well as disassemble words to investigate the actual machine code that our optimizing compiler generates.

In addition to all the open issues about performance, we have an open issue to improve fixnum iteration that would likely help this benchmark and I hope to someday get resolved. And, there are likely many other improvements we could make to our use of tagged fixnum and integer unions or generic dispatch to improve the un-typed arithmetic in the examples above.

Some interesting results on the relative value of different Factor optimizations!

Wed, 16 Jul 2025 15:00:00

John Benediktsson: Command Arguments

A question was asked recently on the Factor mailing list about the Argument Parser that I had previously implemented in Factor:

I have been trying to hack on command-line.parser to add the ability to call it with commands.

The specific feature they want is similar to the ArgumentParser.add_subparsers function in Python’s argparse module. I spent a little bit of time thinking about a quick implementation that can get us started, and applied this patch to support commands.

Here’s their example of a MAIN with two commands with different options using with-commands:

MAIN: [
    H{
        {
            "add"
            {
                T{ option
                    { name "a" }
                    { type integer }
                    { #args 1 }
                }
            }
        }
        {
            "subtract"
            {
                T{ option
                    { name "s" }
                    { type integer }
                    { #args 1 }
                }
            }
        }
    } [ ] with-commands
]

We currently produce no output by default when no command is specified:

$ ./factor foo.factor

The default help prints the possible commands:

$ ./factor foo.factor --help
Usage:
    factor foo.factor [--help] [command]

Arguments:
    command    {add,subtract}

Options:
    --help    show this help and exit

Or get default help for a command:

$ ./factor foo.factor add --help
Usage:
    factor foo.factor add [--help] [a]

Arguments:
    a

Options:
    --help    show this help and exit

Or print an error if the argument is not a valid command:

$ ./factor foo.factor multiply
ERROR: Invalid value 'multiply' for option 'command'

There are other features we might want to add to this including per-command metadata with a brief description of the command, support for additional top-level options besides just the command, and perhaps a different way of handling the no command case rather than empty output.

This is available in the latest developer version!

Thu, 10 Jul 2025 15:00:00

John Benediktsson: Fibonacci Style

About 14 years ago, I wrote about Fibonacci Wars which described the relative performance of three different methods of calculating Fibonacci numbers. Today, I wanted to address a style question that someone in the Factor Discord server asked:

How could I write this better?

: fib ( n -- f(n) ) 0 1 rot 1 - [ tuck + ] times nip ;

In a more concatenative style.

I’ve written before about conciseness, concatenative thinking and readability. I found this question to be a good prompt that provides another opportunity to address these topics.

Their suggested solution is an iterative one and is fairly minimal when it comes to “short code”. It uses less common shuffle words like tuck that users might not understand easily. It is probably true that even rot is more inscrutable to people coming from other languages.

Let’s look at some potential variations!

You could use simpler stack shuffling:

: fib ( n -- f(n) )
    [ 1 0 ] dip 1 - [ over + swap ] times drop ;

You could factor out the inner logic to another word:

: fib+ ( f(n-2) f(n-1) -- f(n-1) f(n) )
    [ + ] keep swap ;

: fib ( n -- f(n) )
    [ 0 1 ] dip 1 - [ fib+ ] times nip ;

You could use higher-level words like keepd:

: fib ( n -- f(n) )
    [ 1 0 ] dip 1 - [ [ + ] keepd ] times drop ;

You could use locals and use index 0 as the “first” fib number:

:: fib ( n -- f(n) )
    1 0 n [ [ + ] keepd ] times drop ;

You could write a recursive solution using memoization for improved performance:

MEMO: fib ( n -- f(n) )
    dup 2 < [ drop 1 ] [ [ 2 - fib ] [ 1 - fib ] bi + ] if ;

You could use local variables to make it look nicer:

MEMO:: fib ( n -- f(n) )
    n 2 < [ 1 ] [ n 2 - fib n 1 - fib + ] if ;

But, in many cases, beauty is in the eye of the beholder. And so you could start at a place where you find the code most readable, and that might even be something more conventional looking like this version that uses mutable locals and comments and whitespace to describe what is happening:

:: fib ( n -- f(n) )
    0 :> f(n-1)!
    1 :> f(n)!

    ! loop to calculate
    n [
        ! compute the next number
        f(n-1) f(n) + :> f(n+1)

        ! save the previous
        f(n) f(n-1)!

        ! save the next
        f(n+1) f(n)!
    ] times

    ! return the result
    f(n) ;

Are any of these clearly better than the original version?

Are there other variations we should consider?

There are often multiple competing priorities when improving code style – including readability, performance, simplicity, and aesthetics. I encourage everyone to spend some time iterating on these various axes as they learn more about Factor!

Tue, 8 Jul 2025 15:00:00

John Benediktsson: Jaro-Winkler

Jaro-Winkler distance is a measure of string similarity and edit distance between two sequences:

The higher the Jaroโ€“Winkler distance for two strings is, the less similar the strings are. The score is normalized such that 0 means an exact match and 1 means there is no similarity. The original paper actually defined the metric in terms of similarity, so the distance is defined as the inversion of that value (distance = 1 โˆ’ similarity).

There are actually two different concepts – and RosettaCode tasks – implied by this algorithm:

  1. Jaro similarity and Jaro distance
  2. Jaro-Winkler similarity and Jaro-Winkler distance.

Let’s build an implementation of these in Factor!

Jaro Similarity

The base that all of these are built upon is the Jaro similarity. It is calculated as a score by measuring the number of matches (m) between the strings, counting the number of transpositions divided by 2 (t), and then returning a weighted score using the formula using the lengths of each sequence (|s1| and |s2|):

In particular, it considers a matching character to be one that is found in the other string within a match distance away, calculated by the formula:

There are multiple ways to go about this, with varying performance, but I decided one longer function was simpler to understand than breaking out the steps into their own words. We use a bit-array to efficiently track which characters have been matched already as we iterate:

:: jaro-similarity ( s1 s2 -- n )
    s1 s2 [ length ] bi@       :> ( len1 len2 )
    len1 len2 max 2/ 1 [-]     :> delta
    len2 <bit-array>           :> flags

    s1 [| ch i |
        i delta [-]            :> from
        i delta + 1 + len2 min :> to

        from to [| j |
            j flags nth [ f ] [
                ch j s2 nth = dup j flags set-nth
            ] if
        ] find-integer-from
    ] filter-index

    [ 0 ] [
        [ length ] keep s2 flags [ nip ] 2filter [ = not ] 2count
        :> ( #matches #transpositions )

        #matches len1 /f #matches len2 /f +
        #matches #transpositions 2/ - #matches /f + 3 /
    ] if-empty ;

The Jaro distance is then just a subtraction:

: jaro-distance ( s1 s2 -- n )
    jaro-similarity 1.0 swap - ;

I’m curious if anyone else has a simpler implementation – please share!

Jaro-Winkler Similarity

The Jaro-Winkler similarity builds upon this by factoring in the length of the common prefix (l) times a constant scaling factor (p) that is usually set to 0.1 in most implementations I’ve seen:

We can implement this by calcuting the Jaro similarity and then computing the common prefix and then generating the result:

:: jaro-winkler-similarity ( s1 s2 -- n )
    s1 s2 jaro-similarity :> jaro
    s1 s2 min-length 4 min :> len
    s1 s2 [ len head-slice ] bi@ [ = ] 2count :> #common
    1 jaro - #common 0.1 * * jaro + ;

The Jaro-Winkler distance is again just a subtraction:

: jaro-winkler-distance ( a b -- n )
    jaro-winkler-similarity 1.0 swap - ;

Try it out

The Wikipedia article compares the similarity of FARMVILLE and FAREMVIEL:

IN: scratchpad "FARMVILLE" "FAREMVIEL" jaro-similarity .
0.8842592592592592

We can also see that the algorithm considers the transposition of two close characters to be less of a penalty than the transposition of two characters farther away from each other. It also penalizes additions and substitutions of characters that cannot be expressed as transpositions.

IN: scratchpad "My string" "My tsring" jaro-winkler-similarity .
0.9740740740740741

IN: scratchpad "My string" "My ntrisg" jaro-winkler-similarity .
0.8962962962962963

We can compare the rough performance of Julia using the same algorithm:

julia> using Random

julia> s = randstring(10_000)

julia> t = randstring(10_000)

julia> @time jarowinklerdistance(s, t)
  1.492011 seconds (108.32 M allocations: 2.178 GiB, 1.87% gc time)
0.19016926812348256

Note: I’m not a Julia developer, I just play one on TV. I adapted this implementation in Julia, which originally took over 4.5 seconds. A better developer could probably improve it quite a bit. In fact, it was pointed out that we are indexing UTF-8 String in a loop, and should instead collect the Char into a Vector first. That does indeed make it super fast.

To the implementation in Factor that we built above, which runs quite a bit faster:

IN: scratchpad USE: random.data

IN: scratchpad 10,000 random-string
               10,000 random-string
               gc [ jaro-winkler-distance ] time .
Running time: 0.259643166 seconds

0.1952856823031448

Thats not bad for a first version that uses safe indexing with unnecessary bounds-checking, generic iteration on integers when usually the indices are fixnum (something I hope to fix someday automatically), and should probably order the input sequences by length for consistency.

If we fix those problems, it gets even faster:

IN: scratchpad USE: random.data

IN: scratchpad 10,000 random-string
               10,000 random-string
               gc [ jaro-winkler-distance ] time .
Running time: 0.068086625 seconds

0.19297898770334765

This is available in the development version in the math.similarity and the math.distances vocabularies.

Sat, 21 Jun 2025 15:00:00

John Benediktsson: Best Shuffle

The “Best shuffle” is a Rosetta Code task that was not yet implemented in Factor:

Task

Shuffle the characters of a string in such a way that as many of the character values are in a different position as possible.

A shuffle that produces a randomized result among the best choices is to be preferred. A deterministic approach that produces the same sequence every time is acceptable as an alternative.

Display the result as follows:

original string, shuffled string, (score)

The score gives the number of positions whose character value did not change.

There are multiple ways to approach this problem, but the way that most solutions seem to take is to shuffle two sets of indices, and then iterate through them swapping the characters in the result if they are different.

I wanted to contribute a solution in Factor, using local variables and short-circuit combinators:

:: best-shuffle ( str -- str' )
    str clone :> new-str
    str length :> n
    n <iota> >array randomize :> range1
    n <iota> >array randomize :> range2

    range1 [| i |
        range2 [| j |
            {
                [ i j = ]
                [ i new-str nth j new-str nth = ]
                [ i str nth j new-str nth = ]
                [ i new-str nth j str nth = ]
            } 0|| [
                 i j new-str exchange
            ] unless
        ] each
    ] each

    new-str ;

And we can write some code to display the result as requested:

: best-shuffle. ( str -- )
    dup best-shuffle 2dup [ = ] 2count "%s, %s, (%d)\n" printf ;

And then print some test cases:

IN: scratchpad {
                   "abracadabra"
                   "seesaw"
                   "elk"
                   "grrrrrr"
                   "up"
                   "a"
               } [ best-shuffle. ] each
abracadabra, raabaracdab, (0)
seesaw, easwse, (0)
elk, lke, (0)
grrrrrr, rrrrgrr, (5)
up, pu, (0)
a, a, (1)

This is reminiscent to the recent work I had done on derangements and generating a random derangement. While this approach does not generate a perfect derangement of the indices – and happens to be accidentally quadratic – it is somewhat similar with the additional step that we look to make sure not only are the indices different, but that the contents are different as well before swapping.

Wed, 18 Jun 2025 15:00:00

John Benediktsson: Dotenv

Dotenv is an informal file specification, a collection of implementations in different languages, and an organization providing cloud-hosting services. They describe the .env file format and some extensions:

The .env file format is central to good DSX and has been since it was introduced by Heroku in 2012 and popularized by the dotenv node module (and other libraries) in 2013.

The .env file format starts where the developer starts - in development. It is added to each project but NOT committed to source control. This gives the developer a single secure place to store sensitive application secrets.

Can you believe that prior to introducing the .env file, almost all developers stored their secrets as hardcoded strings in source control. That was only 10 years ago!

Besides official and many unofficial .env parsers available in a lot of languages, the Dotenv organization provides support for dotenv-vault cloud services in Node.js, Python, Ruby, Go, PHP, and Rust.

Today, I wanted to show how you might implement a .env parser in Factor.

File Format

The .env files are relatively simple formats with key-value pairs that are separated by an equal sign. These values can be un-quoted, single-quoted, double-quoted, or backtick-quoted strings:

SIMPLE=xyz123
INTERPOLATED="Multiple\nLines"
NON_INTERPOLATED='raw text without variable interpolation'
MULTILINE = `long text here,
e.g. a private SSH key`

Parsing

There are a lot of ways to build a parser – everything from manually spinning through bytes using a hand-coded state machine, higher-level parsing grammars like PEG, or explicit parsing syntax forms like EBNF.

We are going to implement a .env parser using standard PEG parsers, beginning with some parsers that look for whitespace, comment lines, and newlines:

: ws ( -- parser )
    [ " \t" member? ] satisfy repeat0 ;

: comment ( -- parser )
    "#" token [ CHAR: \n = not ] satisfy repeat0 2seq hide ;

: newline ( -- parser )
    "\n" token "\r\n" token 2choice ;

Keys

The .env keys are specified simply:

For the sake of portability (and sanity), environment variable names (keys) must consist solely of letters, digits, and the underscore ( _ ) and must not begin with a digit. In regex-speak, the names must match the following pattern:

[a-zA-Z_]+[a-zA-Z0-9_]*

We can build a key parser by looking for those characters:

: key-parser ( -- parser )
    CHAR: A CHAR: Z range
    CHAR: a CHAR: z range
    [ CHAR: _ = ] satisfy 3choice

    CHAR: A CHAR: Z range
    CHAR: a CHAR: z range
    CHAR: 0 CHAR: 9 range
    [ CHAR: _ = ] satisfy 4choice repeat0

    2seq [ first2 swap prefix "" like ] action ;

Values

The .env values can be un-quoted, single-quoted, double-quoted, or backtick-quoted strings. Only double-quoted strings support escape characters, but single-quoted and backtick-quoted strings support escaping either single-quotes or backtick characters.

: single-quote ( -- parser )
    "\\" token hide [ "\\'" member? ] satisfy 2seq [ first ] action
    [ CHAR: ' = not ] satisfy 2choice repeat0 "'" dup surrounded-by ;

: backtick ( -- parser )
    "\\" token hide [ "\\`" member? ] satisfy 2seq [ first ] action
    [ CHAR: ` = not ] satisfy 2choice repeat0 "`" dup surrounded-by ;

: double-quote ( -- parser )
    "\\" token hide [ "\"\\befnrt" member? ] satisfy 2seq [ first escape ] action
    [ CHAR: " = not ] satisfy 2choice repeat0 "\"" dup surrounded-by ;

: literal ( -- parser )
    [ " \t\r\n" member? not ] satisfy repeat0 ;

Before we implement our value parser, we should note that some values can be interpolated:

Interpolation (also known as variable expansion) is supported in environment files. Interpolation is applied for unquoted and double-quoted values. Both braced (${VAR}) and unbraced ($VAR) expressions are supported.

Direct interpolation

  • ${VAR} -> value of VAR

Default value

  • ${VAR:-default} -> value of VAR if set and non-empty, otherwise default
  • ${VAR-default} -> value of VAR if set, otherwise default

And some values can have command substitution:

Add the output of a command to one of your variables in your .env file. Command substitution is applied for unquoted and double-quoted values.

Direct substitution

  • $(whoami) -> value of $ whoami

We can implement an interpolate parser that acts on strings and replaces observed variables with their interpolated or command-substituted values. This uses a regular expressions and re-replace-with to substitute values appropriately.

: interpolate-value ( string -- string' )
    R/ \$\([^)]+\)|\$\{[^\}:-]+(:?-[^\}]*)?\}|\$[^(^{].+/ [
        "$(" ?head [
            ")" ?tail drop process-contents [ blank? ] trim
        ] [
            "${" ?head [ "}" ?tail drop ] [ "$" ?head drop ] if
            ":-" split1 [
                [ os-env [ empty? not ] keep ] dip ?
            ] [
                "-" split1 [ [ os-env ] dip or ] [ os-env ] if*
            ] if*
        ] if
    ] re-replace-with ;

: interpolate ( parser -- parser )
    [ "" like interpolate-value ] action ;

We can use that to build a value parser, remembering that only un-quoted and double-quoted values are interpolated, and making sure to convert the result to a string:

: value-parser ( -- parser )
    [
        single-quote ,
        double-quote interpolate ,
        backtick ,
        literal interpolate ,
    ] choice* [ "" like ] action ;

Key-Values

Combining those, we can make a key-value parser, that ignores whitespace around the = token and uses set-os-env to update the environment variables:

: key-value-parser ( -- parser )
    [
        key-parser ,
        ws hide ,
        "=" token hide ,
        ws hide ,
        value-parser ,
    ] seq* [ first2 swap set-os-env ignore ] action ;

And finally, we can build a parsing word that looks for these key-value pairs while ignoring optional comments and whitespace:

PEG: parse-dotenv ( string -- ast )
    ws hide key-value-parser optional
    ws hide comment optional hide 4seq
    newline list-of hide ;

Loading Files

We can load a file by reading the file-contents and then parsing it into environment variables:

: load-dotenv-file ( path -- )
    utf8 file-contents parse-dotenv drop ;

These .env files are usually located somewhere above the current directory, typically at a project root. For now, we make a word that traverses from the current directory up to the root, looking for the first .env file that exists:

: find-dotenv-file ( -- path/f )
    f current-directory get absolute-path [
        nip
        [ ".env" append-path dup file-exists? [ drop f ] unless ]
        [ ?parent-directory ] bi over [ f ] [ dup ] if
    ] loop drop ;

And now, finally, we can find and then load the relevant .env file, if there is one:

: load-dotenv ( -- )
    find-dotenv-file [ load-dotenv-file ] when* ;

Try it out

We can make a simple .env file:

$ cat .env
HOST="${HOST:-localhost}"
PORT="${PORT:-80}"
URL="https://${HOST}:${PORT}/index.html"

And then try it out, overriding the PORT environment variable:

$ PORT=8080 ./factor
IN: scratchpad USE: dotenv
IN: scratchpad load-dotenv
IN: scratchpad "URL" os-env .
"https://localhost:8080/index.html"

Some additional features that we might want to follow up on:

This is available in the latest development version. Check it out!

Tue, 17 Jun 2025 15:00:00

John Benediktsson: Color Prettyprint

Factor has a neat feature in the prettyprint vocabulary that allows printing objects, typically as valid source literal expressions. There are small caveats to that regarding circularity, depth limits, and other prettyprint control variables, but it’s roughly true that you can pprint most everything and have it be useful.

At some point in the past few years, I noticed that Xcode and Swift Playground have support for color literals that are rendered in the source code. You can see that in this short video describing how it works:

Inspired by that – and a past effort at color tab completion – I thought it would be fun to show how you might extend our color support to allow colors to be prettyprinted with a little gadget in the UI that renders their colors.

First, we need to define a section object that holds a color and renders it using a colored border gadget.

TUPLE: color-section < section color ;

: <color-section> ( color -- color-section )
    1 color-section new-section swap >>color ;

M: color-section short-section
     " " <label> { 5 0 } <border>
        swap color>> <solid> >>interior
        COLOR: black <solid> >>boundary
    output-stream get write-gadget ;

Next, we extend pprint* with a custom implementation for any color type as well as our named colors that adds a color section to the output block:

M: color pprint*
    <block
        [ call-next-method ]
        [ <color-section> add-section ] bi
    block> ;

M: parsed-color pprint*
    <block
        [ \ COLOR: pprint-word string>> text ]
        [ <color-section> add-section ] bi
    block> ;

And, now that we have that, we can push some different colors to the stack and see how they are all displayed:

Pretty cool.

I did not commit this yet – partly because I’m not sure we want this as-is and also partly because it needs to only display the gadget if the UI is running. We also might want to consider the UI theme and choose a nice contrasting color for the border element.

Sun, 15 Jun 2025 15:00:00

Blogroll


planet-factor is an Atom/RSS aggregator that collects the contents of Factor-related blogs. It is inspired by Planet Lisp.

Syndicate