Changelog
v1.14.0 (2026)
Theme: Frankie for Real Web Apps
New Language Features
spawn { } — Background Blocks
- Run any block in a background thread — returns immediately, response goes out while work continues
- Works in web routes and standalone scripts
- Spawned blocks receive a copy of variables at spawn time; mutations do not affect outer scope
- Backed by Python's threading.Thread — zero dependencies
timeout(n) { } — Time-Bounded Execution
- Kill any block that exceeds n seconds — raises TimeoutError
- Works inside begin/rescue TimeoutError for graceful fallback
- Essential for external HTTP calls, slow database queries, and any operation that can hang
- Backed by Python's threading with a sentinel thread — zero dependencies
Hash Destructuring — {name, age} = user
- Pull hash keys directly into local variables in one assignment
- Missing keys evaluate to nil — no error
- Works anywhere an assignment is valid: top level, inside functions, inside route handlers, inside loops
- Bareword (symbol) keys only — matches the existing hash literal convention
Shape Pattern Matching — case user when {role: "admin"}
- when clauses now accept hash literals — matches any hash containing at least those key/value pairs
- Extra keys in the subject hash are ignored (subset match)
- Works with records — record Point(x, y) is a hash, so when {x: 0} is valid
- Mix shape and value when clauses in the same case
New Web Features
Async Routes — app.get_async / app.post_async etc.
- Non-blocking route handlers — slow I/O in one handler does not hold up others
- All five HTTP methods have async variants: get_async, post_async, put_async, delete_async, patch_async
- Use await inside async blocks for non-blocking calls
Middleware Stack — app.use do |req, next_fn|
- Chain middleware that wraps every request — auth, logging, rate-limiting, CORS
- Each layer calls next_fn.(req) to pass control forward, or returns a response to short-circuit
- Runs in registration order
Static File Serving — app.static(dir) / app.static(dir, prefix)
- Serve a directory of static files with one line
- Serves HTML, CSS, JS, images, fonts, JSON automatically
- Optional URL prefix: app.static("./assets", "/static")
- Directory listing disabled by default
New Stitches
frankietemplate
- Mustache-compatible template engine — zero dependencies, pure .fk
- {{ variable }} — HTML-escaped interpolation
- {{{ variable }}} — raw / unescaped output
- {{# section }} ... {{/ section}} — truthy blocks and vector iteration
- {{^ inverted }} ... {{/ inverted}} — falsy / empty blocks
- {{> partial_name }} — include from ./views/partials/<n>.html
- {{! comment }} — stripped from output
- render(template, data) — render a string
- render_file(path, data) — load and render a file
- partial(name) — load a partial by name
frankiecookie
- HMAC-SHA256 signed cookies via Python's hmac + hashlib stdlib — zero dependencies
- set_signed_cookie(resp, name, value, secret, opts) — write a tamper-proof cookie
- get_signed_cookie(req, name, secret) — read and verify — returns nil if missing or tampered
- delete_cookie(resp, name) — expire a cookie immediately
- cookie_set?(req, name) — check if a cookie is present
- Supports all cookie options: path, max_age, same_site, http_only, secure
Tooling
frankiec new — Scaffold Updated for Stitches
- Generated project now includes a stitches/ folder with a README.md explaining the stitch convention
- Generated project includes a views/partials/ folder for template projects
- Generated README.md documents stitch "name" usage
- Version string in main.fk banner updated to v1.14
Global Stitch Install — install.py
- install.py now correctly copies all bundled stitches to ~/.frankie/stitches/ at install time
- Installation output lists each stitch file copied
- frankiecookie and frankietemplate included in the bundled set
- python3 install.py --uninstall removes ~/.frankie/stitches/ and ~/.frankie/ if empty
v1.13.1 (2026)
Bug Fixes & Gap Closers
Parser / Compiler
- Heredoc inside
do...endblocks — Fixed lexer bug that discarded tokens after<<~DELIMon the same line. Heredocs now work anywhere a string expression is valid, including inside route and iterator blocks. The only documented workaround in the language is removed. - Vector
.sum do |x| ... end— Block form was swallowed by the internal method map before block detection. Now correctly routes to_fk_sum_byfor projected sums. - Vector
.flat_map do |x| ... end— Multi-line block bodies now parse correctly. Hash.each do |k, v|— Two-parameter block iteration confirmed and end-to-end tested. No workaround needed.
Standard Library
assert_approx_eq(actual, expected, delta, msg)— Float comparison assertion with configurable delta (default0.001). Replacesassert_true(abs(a-b) < delta, ...)boilerplate.run_tests()— Now a public stdlib function callable from any.fkfile, not justfrankiec test.session(req, resp)— Cookie-backed session helper. Read, mutate, and write back with.save(). Single JSON cookie (_fk_session), zero server-side state.- String
.ljust(n)/.rjust(n)/.center(n)— Promoted to documented stdlib status with examples. - String
.start_with?(s)/.end_with?(s)— Documented as first-class predicates. - Hash
.keys/.values/.has_key?(k)— All three consistently documented;.valuesand.has_key?reference gaps closed.
Stitches
frankiestringv2 — Rewritten with a clean API. Oldlfill/rfillreplaced bypad_left(str, n, char), pad_right(str, n, char), truncate(str, n, suffix), slugify(str), word_wrap(str, width), indent_lines(str, n).
Tooling
frankiec fmt— symbol key round-trip — Symbol keys (host: "x") are preserved after formatting;{host: "val"}no longer becomes{"host": "val"}.frankiec fmt— blank line preservation — Intentional blank lines between statement groups inside function bodies are now preserved.frankiec fmt— multi-line threshold — Hashes and vectors whose inline form exceeds 60 characters auto-expand to one element per line.frankiec fmt— idempotency — Runningfmt --writetwice now produces identical output. Safe for pre-commit hooks and CI.
REPL
- Multi-line REPL input — Fixed
_is_incompleteedge cases: standalonedo |x|lines now correctly hold the...prompt open; comment lines are skipped during depth counting.
v1.13.0 (2026)
New Features
Language — stitch "name" keyword
- New keyword for loading third-party Frankie packages by name
- Resolution order: ./stitches/<n>.fk (project-local) → ~/.frankie/stitches/<n>.fk (user-global)
- Friendly error when not found: tells you exactly where to put the file
- Each stitch is loaded at most once — safe to call multiple times
- Uses the same underlying require machinery — stitch files are plain .fk files
- Establishes a clear convention: lib/ = your code, stitches/ = third-party packages
Language — ? in user-defined function names
- def even?(n) and def palindrome?(s) now work correctly
- ? is compiled to _q in generated Python — transparent to the programmer
- Applies to function definitions, calls, and assignments
Stitch — frankieforms
- Form field validation returning a Hash of {field: error_message} pairs
- Rules: required, min_length, max_length, email, min_value, max_value, numeric, alpha, matches_pattern
- validate(form, rules) → error hash · valid?(form, rules) → boolean
Stitch — frankietable
- ASCII table rendering from a vector of hashes
- table(rows) — all columns · table(rows, cols) — specific columns in given order
- Column widths auto-sized to content
Stitch — frankiecolor
- ANSI color and style helpers for terminal output
- Color functions: red, green, yellow, blue, cyan, magenta, white, black
- Style functions: bold, dim, italic, underline, inverse
- Semantic helpers: success, error, warn, info
- colorize(str, color) — generic · strip_color(str) — remove ANSI codes
Stitch — frankiepager
- Pagination math for web apps and CLI tools
- paginate(opts) → full pager hash with page, total_pages, from, to, has_prev, has_next, prev_page, next_page
- page_slice(items, page, per_page) — slice a vector to the current page
- page_links(pager, url_template) — navigation link vector
Stitch — frankieconfig
- Layered configuration loading: defaults → JSON file → environment variables → overrides
- Type coercion from env var strings to match default types (Integer, Float, Boolean)
- load_config(opts) · config_get(config, key, fallback)
v1.12.0 (2026)
New Features
Standard Library — String .gsub with Block
- "hello".gsub("[aeiou]") do |m| m.upcase end → "hEllO"
- Block form transforms each match; the block receives the matched substring and returns the replacement
- The fixed-string form gsub(pattern, replacement) continues to work unchanged
- Uses re.sub with a callable internally — no new dependencies
Standard Library — Hash .map_hash do |k, v|
- {a: 1, b: 2}.map_hash do |k, v| [k, v * 2] end → {a: 2, b: 4}
- Transforms a hash into a new hash in one idiomatic step
- Block must return a two-element vector [new_key, new_value]; any other return raises a clear runtime error
- Fills the gap between .map (returns vector of pairs) and a true hash transform
Standard Library — round(x, n)
- round(3.14159, 2) → 3.14
- Rounds to n decimal places; n defaults to 0
- Available as a top-level function alongside floor and ceil
- Wired to Python's built-in round() — no surprises on banker's rounding edge cases
Standard Library — Vector .product(other)
- [1,2].product([3,4]) → [[1,3],[1,4],[2,3],[2,4]]
- Cartesian product — every combination of elements from two vectors
- Pure nested loop, zero dependencies
- Natural companion to .zip and .zip_with — completes the combinatorics trio
Standard Library — String .chars (promoted)
- .chars was already in the method map but undocumented
- Now a first-class documented method alongside .bytes and .lines
- Chains naturally into iterators: "hello".chars.select do |c| c != "l" end
Standard Library — Vector .each_with_object with Hash Accumulator (documented)
- Hash accumulators already worked implicitly; now explicitly documented and tested
- [1,2,3].each_with_object({}) do |x, h| h[x] = x * x end → {1: 1, 2: 4, 3: 9}
Tooling — assert_match and assert_nil
- assert_match(value, pattern, msg) — checks a regex match; pattern can be a regex or string
- assert_nil(value, msg) — checks for nil; cleaner than assert_eq(x, nil)
- Both available in frankiec test with the same output style as existing assertions
Tooling — frankiec watch <file.fk>
- Polls file mtime and re-runs automatically on every save
- frankiec watch test.fk --test runs in test mode
- Zero dependencies — uses os.stat and time.sleep
- Ctrl-C to stop; gracefully ignores exit() calls in watched files
Tooling — frankiec repl --no-banner
- Skips the ASCII art and version header
- Makes the REPL usable piped into other tools or embedded in scripts
Bug Fixes & Improvements
Runtime — FileNotFoundError from File I/O
- file_read and file_lines previously raised RuntimeError, silently defeating rescue FileNotFoundError
- Now raise a genuine FileNotFoundError with a clean message — no [Frankie] prefix noise
- file_copy and file_rename also raise FileNotFoundError when the source is missing
- file_delete and file_exists unchanged — returning false for missing files is correct for those
- rescue FileNotFoundError e now works as expected for all file I/O
Runtime — Friendlier Error Messages
- TypeError now reads as "Type mismatch — can't use '+' with Integer and String" instead of the raw Python message
- IndexError now reads as "Index out of bounds — vector index does not exist" instead of "list index out of range"
- FileNotFoundError strips the raw Python [Errno 2] No such file or directory: prefix
- The friendly dict in frankiec.py is now backed by three focused helper functions for easier future extension
v1.11.0 (2026)
New Features
Language — Implicit Return
- The last expression in a function body is now automatically returned — return is optional
- Early return statements are still fully supported for mid-function exits
- Applies to all function bodies including nested functions
- Only expressions trigger implicit return; assignments, loops, and puts at the end still return nil
- Zero breaking change risk: all existing programs using explicit return continue to work identically
Language — Inline if Expression
- x = if cond then a else b end — if is now usable as an expression, not just a statement
- then keyword is optional; a newline after the condition also works
- elsif clauses are supported: if a then x elsif b then y else z end
- Missing else clause evaluates to nil
- New THEN token type added to the lexer; new IfExpr AST node added
- Avoids introducing a ?: ternary operator that clashes with Frankie's readable style
Standard Library — String .replace(old, new)
- "hello world".replace("world", "Frankie") → "hello Frankie"
- Replaces the first occurrence — an alias for sub()
- The method name new users always reach for before remembering sub/gsub
- sub(), gsub() continue to work as before
Standard Library — String .format(hash)
- "Hello, {name}!".format({name: "Alice"}) — named {key} placeholder replacement
- Method form of the existing template() function
- Uses {key} syntax (vs template()'s {{key}} syntax)
- Runtime dispatches on argument type: dict → string format; non-dict → datetime format
Standard Library — .zip_with do |a, b|
- [1,2,3].zip_with([10,20,30]) do |a, b| a + b end → [11, 22, 33]
- Pair-wise transform two vectors in a single pass
- Completes the R-style vector pipeline alongside .zip, .map, .select
- Stops at the shorter vector, matching .zip behaviour
Tooling — frankiec check Boxed Error Output
- Parse and lex errors from frankiec check now use the same boxed format as runtime errors
- Includes file path, line number, and source context with ──▶ pointer
- Essential for editor integration — output is now machine-parseable and visually consistent
- Exit codes unchanged: 0 = clean, 1 = error
Tooling — REPL Multi-line History Recall
- ↑ now recalls a complete def...end block as a single history entry
- Previously, each line of a multi-line block was stored separately
- Implementation: per-line readline entries are removed and replaced with a single joined entry
- History file (~/.frankie_history) updated on block submission, not just on exit
- Gracefully degrades on readline bindings that don't support remove_history_item
Tooling — frankiec fmt Heredoc Support
- Heredoc string bodies are now preserved verbatim during formatting
- Multiline string literals are re-emitted as <<~HEREDOC blocks
- Fixes a v1.10 regression where heredoc content could be mangled by the formatter
Documentation
- New
docs/13_v111_features.mdwith full feature reference and examples - Multiple return values via destructuring documented as an official pattern
- String
.delete(chars)promoted from hidden stdlib to documented method
v1.10.0 (2026)
New Features
Language — String & Vector * Repetition
- "ha" * 3 → "hahaha" — string repetition
- [0] * 5 → [0, 0, 0, 0, 0] — vector fill
- [1, 2] * 3 → [1, 2, 1, 2, 1, 2] — pattern repeat
- Integer on either side works: 3 * "hi" → "hihihi"
- Implemented in _fk_arith — zero new syntax
Language — Heredoc <<~TEXT
- <<~DELIM ... DELIM multiline string with automatic indent-stripping
- <<DELIM variant (no strip) also supported
- Full #{} interpolation inside heredoc bodies
- Pure lexer change — no new tokens or AST nodes
- The codegen gen_string rewritten to use repr() + concatenation for multiline interpolated strings, eliminating triple-quote/backslash edge cases entirely
Language — Named Rescue Without Variable
- rescue TypeError is now valid without a binding variable
- rescue TypeError e still works when the message is needed
- Parser fix: variable binding is now truly optional after a typed rescue
Standard Library — times(n) do |i| standalone
- times(n) do |i| ... end functional form added alongside n.times do
- times(n) with no block returns [0..n-1] as a list
- FuncCall AST node gains an optional block field; parser attaches trailing do...end blocks to function calls; codegen emits a for loop for times
Standard Library — flatten(depth)
- .flatten with no argument now does full deep flatten (breaking change from v1.9's one-level-only behaviour)
- .flatten(n) flattens exactly n levels; .flatten(0) is a no-op
- Backed by new _fk_flatten_deep(iterable, depth) in stdlib
Standard Library — map_with_index
- .map_with_index do |x, i| ... end — index available in map block
- Single-expression blocks compile to a list comprehension; multi-statement blocks use a helper function
Standard Library — pp(value) pretty-print
- Indented multiline output for hashes, vectors, and records
- Records printed as RecordName(\n field: value,\n ...)
- Flat vectors printed on one line; nested structures indented recursively
Standard Library — encode / decode
- "hello".encode → [104, 101, 108, 108, 111] (UTF-8 bytes as vector)
- "hello".encode("ascii") — explicit encoding
- [104, 105].decode → "hi"
- [104, 105].decode("utf-8") — explicit encoding
Runtime — Exit Code Propagation
- exit(42) in Frankie code now propagates the exact code to the shell
- frankiec run catches SystemExit and calls sys.exit(e.code) instead of re-raising
CLI — --help Flag
- frankiec --help prints the full usage docstring
- frankiec <cmd> --help prints a short description for that specific command
- All commands covered: run, repl, test, fmt, docs, build, check, new, version
Bug Fixes
gen_stringmultiline interpolation: single-line f-strings were emitted with literal embedded newlines (invalid Python syntax). Fixed by usingrepr()+ string concatenation for all multiline interpolated strings.flattensemantics changed to full-deep by default; use.flatten(1)for the old one-level behaviour.
v1.9.0 (2026)
New Features
Language — Record Types (record)
- record Point(x, y) defines a lightweight named data object
- Constructor function generated automatically: p = Point(3, 4)
- Prints as Point(x: 3, y: 4) — clean, readable output
- Records are hashes under the hood — all hash methods, iterators, dig, |, and .merge work on them
- New lexer token RECORD, new AST node RecordDef, new gen_record_def in codegen
- _fk_to_str updated to detect __type__ and display record notation
record Employee(name, dept, salary)
emp = Employee("Alice", "Engineering", 95000)
puts emp # Employee(name: Alice, dept: Engineering, salary: 95000)
puts emp["dept"] # Engineering
by_dept = employees.group_by do |e| e["dept"] end
Standard Library — hash.dig(key, ...)
- Safe nested access: returns nil at the first missing key, never crashes
- Works on hashes (string/symbol keys) and vectors (integer indices)
- Chains correctly with &. for nil-safe navigation
config = {db: {host: "localhost", pool: {max: 10}}}
puts config.dig("db", "pool", "max") # 10
puts config.dig("db", "missing") # nil (no crash)
Standard Library — Standalone zip(*vecs)
- zip(a, b) function form alongside the existing .zip method
- Accepts two or more vectors; stops at the shortest
- Consistent with Frankie's R-inspired functional style
zip(["Alice", "Bob"], [95, 87]) # [["Alice", 95], ["Bob", 87]]
Tooling — frankiec fmt (Auto-Formatter)
- New command: frankiec fmt <file.fk> — print canonically formatted source
- --write flag: reformat in-place
- --check flag: exit 1 if not already formatted (CI-friendly)
- Implemented in frankie_fmt.py — walks the AST, zero new dependencies
- Canonical style: 2-space indent, single-expr blocks inlined, blank line after top-level def
Tooling — frankiec docs (Documentation Generator)
- New command: frankiec docs <file.fk> — extract ## doc-comments to Markdown
- --output <file.md> flag: write to file instead of stdout
- Supports @param, @return, @example tags
- Works on directories: frankiec docs lib/ generates docs for every .fk file
- Implemented in frankie_docs.py — pure Python, zero new dependencies
REPL — readline, Tab Completion, History Persistence
- Arrow key navigation (↑/↓) and Ctrl+R reverse-search via Python's built-in readline
- Tab completion for Frankie keywords, stdlib functions, and common method names
- History saved to ~/.frankie_history on exit; restored at next startup (max 1000 entries)
- .env auto-loaded from the current working directory at REPL startup
Runtime — .env Auto-Loader
- frankiec run and frankiec repl automatically load .env from the current directory
- Keys already set in the shell environment take precedence
- Values accessible via the existing env(key, default) stdlib function
- No crash if .env is absent
Bug Fixes
recordadded as a reserved keyword — programs that usedrecordas a variable name must rename it (the existing.fkexamples in the repo have been updated)scaffold.pyversion string updated fromv1.3tov1.9
v1.8.0 (2026)
New Features
Language — Lambda / Anonymous Functions (->)
- Store functions as first-class values: double = ->(x) { x * 2 }
- Call with .call(args): double.call(5) → 10
- Single-expression bodies use brace syntax: ->(x) { x * 2 }
- Multi-statement bodies use do...end: ->(x) do ... end
- Default parameters are supported: ->(x, y = 1) { x + y }
- Lambdas can be stored in variables, vectors, and hashes
- Lambdas can be passed to functions as arguments (higher-order functions)
- Lambdas can be returned from functions
- New token: ARROW (->)
- New AST node: LambdaLiteral
double = ->(x) { x * 2 }
add = ->(a, b) { a + b }
puts double.call(7) # 14
puts add.call(3, 4) # 7
def apply(fn, val)
return fn.call(val)
end
puts apply(double, 9) # 18
Language — Hash Merge Operator |
- h1 | h2 merges two hashes; right-hand keys win on conflict
- Returns a new hash — neither operand is modified
- Chains naturally: a | b | c
- Complements the existing .merge(other) method
defaults = {color: "blue", size: "medium"}
overrides = {color: "red"}
puts defaults | overrides # {color: red, size: medium}
Standard Library — group_by
- vector.group_by do |x| key end buckets elements by block return value
- Returns a hash whose values are arrays of matching elements, in original order
- Pairs naturally with .tally, .sort_by, .each, and the new | operator
words = ["ant", "ape", "bear", "bee", "cat"]
puts words.group_by do |w| w[0] end
# {"a" => ["ant", "ape"], "b" => ["bear", "bee"], "c" => ["cat"]}
Standard Library — each_slice and each_cons
- vector.each_slice(n) — iterate non-overlapping chunks of size n
- vector.each_cons(n) — iterate all consecutive windows of size n (sliding window)
- Both accept an optional do |var| ... end block; without a block, return a vector of slices/windows
- Mirrors Ruby's API; natural fit for data-processing, batch operations, and rolling statistics
[1,2,3,4,5,6].each_slice(2) do |s|
puts s # [1,2] [3,4] [5,6]
end
[10, 13, 11, 15].each_cons(2) do |w|
puts w[1] - w[0] # 3 -2 4
end
Bug Fixes
_block_to_lambda— blocks whose body ends with a control-flow expression (if,case,unless) now correctly capture the result value instead of raising aCodeGenError. Affectsselect,reject,sort_by,min_by,max_by,sum_by,find,flat_map, and the newgroup_by.
v1.7.1 (2025)
Bug Fixes
N.times do—def _gen_timesmethod definition was accidentally dropped fromcompiler/codegen.pyduring v1.7 development, causing anAttributeErrorat runtime. The method body was present but thedefheader was missing.test_v17.fk— example test file useddef()anonymous function syntax insideassert_raises_typedwhich does not exist in Frankie yet. Rewritten to usebegin/rescueblocks instead.
v1.7.0 (2025)
New Features
Language — Nil Safety Operator &.
- x&.method — call .method on x, returning nil if x is nil (no crash)
- x&.method(args) — nil-safe method call with arguments
- x&.property — nil-safe property / zero-arg attribute access
- Chains naturally: a&.b&.c — short-circuits at the first nil
- Works with any value: strings, hashes, vectors, custom objects
- No language keywords added — &. is a single new operator token
user = {name: "Alice"}
missing = nil
puts user["name"]&.upcase # ALICE
puts missing&.upcase # nil (no crash)
puts missing&.upcase&.reverse # nil (chain short-circuits)
Standard Library — template(str, hash)
- Replace {{key}} placeholders in a string with values from a hash
- Clean alternative to sprintf / #{} when keys are dynamic or templates are stored externally
- Raises KeyError if a placeholder key is missing from the hash
msg = template("Hello, {{name}}! Age: {{age}}.", {name: "Alice", age: 30})
puts msg # Hello, Alice! Age: 30.
Standard Library — File System Operations
- file_rename(src, dst) — rename or move a file
- file_copy(src, dst) — copy a file (preserving metadata); returns dst
- file_mkdir(path) — create a directory; creates intermediate dirs by default (like mkdir -p)
- file_mkdir(path, false) — create a single directory only (no parents)
- dir_exists(path) — return true if path is an existing directory
- dir_list(path) — return a sorted vector of filenames in a directory (default: ".")
- All use Python's built-in os / shutil — zero external dependencies
file_mkdir("/tmp/myapp/data")
puts dir_exists("/tmp/myapp/data") # true
file_write("/tmp/myapp/data/a.txt", "hello")
file_copy("/tmp/myapp/data/a.txt", "/tmp/myapp/data/b.txt")
file_rename("/tmp/myapp/data/b.txt", "/tmp/myapp/data/c.txt")
puts dir_list("/tmp/myapp/data") # [a.txt, c.txt]
Standard Library — assert_raises_typed(fn, type, msg)
- Extends the test runner to assert that a specific error type was raised
- type can be a string ("ZeroDivisionError") or a Python exception class
- Fails with a clear message if no error is raised, or if the wrong type is raised
- Supported type names mirror typed rescue: RuntimeError, TypeError, ValueError,
ZeroDivisionError, IndexError, KeyError, IOError, FileNotFoundError,
OverflowError, NameError, AttributeError, Exception / Error
assert_raises_typed(def()
x = 1 // 0
end, "ZeroDivisionError", "division by zero raises correctly")
assert_raises_typed(def()
file_read("/no/such/file.txt")
end, "RuntimeError", "missing file raises RuntimeError")
Bug Fixes
&.operator correctly short-circuits chains — once a nil is encountered, remaining method calls in the chain are skipped without raising errors
v1.6.0 (2025)
New Features
Language — Compound Assignment Operators
- += — add and assign: x += 5
- -= — subtract and assign: x -= 3
- *= — multiply and assign: x *= 2
- /= — divide and assign (float): x /= 4
- //= — integer-divide and assign (Fortran): x //= 3
- **= — exponentiate and assign (Fortran): x **= 2
- %= — modulo and assign: x %= 7
- All operators also work on vector elements: v[i] += 1
Language — Typed Rescue Clauses
- rescue TypeError e — catch only TypeError errors
- rescue ZeroDivisionError e — catch only division by zero
- Multiple rescue clauses on one begin...end block, checked in order
- Full list of supported types: RuntimeError, TypeError, ValueError,
ZeroDivisionError, IndexError, KeyError, IOError,
FileNotFoundError, OverflowError, NameError, AttributeError,
StopIteration, Exception / Error (catch-all aliases)
- Untyped rescue e remains valid and catches everything
Standard Library — .find / .detect
- .find do |x| ... end — return the first element for which the block is true, or nil
- .detect do |x| ... end — alias for .find
- Works on any vector, including vectors of hashes
- Chains naturally with .select, .map, .sort_by, etc.
Tooling — frankiec test
- frankiec test — run test.fk in the current directory
- frankiec test <file.fk> — run a named test file
- Built-in assertions (no imports needed):
- assert_true(cond, msg) — pass if condition is truthy
- assert_eq(actual, expected, msg) — pass if values are equal
- assert_neq(actual, expected, msg) — pass if values differ
- assert_raises(fn, msg) — pass if calling fn raises any error
- Live ✓ / ✗ output per assertion
- Summary line with pass count, fail count, and elapsed time
- Exits with code 1 if any assertion fails (CI-friendly)
Bug Fixes
_fk_test_suitesingleton now uses a fresh isolated copy perfrankiec testrun, preventing state leakage between multiple test invocations in the same process
v1.5.0 (2025)
New Features
Language — Loop Control
- next — skip to the next iteration (like continue in other languages); supports postfix next if cond
- break — exit a loop early; supports postfix break if cond
- break value — exit a loop and store a result in _fk_break_val; supports postfix form
Language — Constants
- UPPER_CASE = value — UPPER_SNAKE_CASE identifiers are treated as constants
- Reassignment prints a warning and preserves the original value
- Works with any type: integers, floats, strings, vectors, hashes
Standard Library — Randomness
- random() — random Float in [0.0, 1.0)
- rand(n) — random Integer in [0, n)
- rand_int(a, b) — random Integer in [a, b] (both inclusive)
- rand_float(a, b) — random Float in [a, b)
- shuffle(vec) — return a shuffled copy of a vector
- sample(vec, n) — return n randomly chosen elements (no repeats)
- rand_seed(n) — seed the RNG for reproducible results
Standard Library — Sorting
- .sort_by do |x| key end — sort a vector by any computed key
- .min_by do |x| key end — element with the smallest key
- .max_by do |x| key end — element with the largest key
- .sum_by do |x| val end — sum the values the block returns
Standard Library — Other
- sleep(n) — pause execution for n seconds (float supported)
- unzip(vec) — inverse of zip: vector of pairs → vector of columns
- format(fmt, ...) — alias for sprintf
Bug Fixes
- Block parameters named
p(e.g.do |p|) now parse correctly —pwas always tokenised as the debug-print keyword, preventing it from being used as a loop variable p[...]andp.methodnow correctly treated as variable access rather than a debug-print callbreak if condandnext if cond(postfix forms) parse without errors
v1.4.0 (2025)
New Features
Web Server (built-in http.server — zero deps)
- web_app() — create a new Frankie web application
- app.get(path) do |req| end — register a GET route
- app.post(path) do |req| end — register a POST route
- app.put(path) do |req| end — register a PUT route
- app.delete(path) do |req| end — register a DELETE route
- app.patch(path) do |req| end — register a PATCH route
- app.before do |req| end — before-filter (runs before every matched route)
- app.after do |req, res| end — after-filter (runs after every matched route)
- app.not_found do |req| end — custom 404 handler
- app.run(port) / app.run(port, host) — start the server (blocking, multi-threaded)
- Path parameters with :name segments: "/users/:id" → req.params["id"]
- Query string access: req.query["page"]
- JSON body parsing: req.json — returns parsed hash/vector or nil
- Form body parsing: req.form — returns decoded hash
- response(body, status, headers) — plain-text response
- html_response(body, status) — HTML response
- json_response(data, status) — JSON response (auto-serializes hashes and vectors)
- redirect(location, status) — redirect response (default 302)
- halt(status, body) — error response shortcut
- Returning a plain string from a handler auto-wraps as 200 text/plain
- Returning a hash or vector auto-wraps as 200 application/json
- Full request object: .method, .path, .params, .query, .headers, .body, .json, .form
- See docs/09_web.md and examples/webapp.fk for full reference and demo
Bug Fixes
raise expr if cond(postfixifonraise) now parsed correctly — was leaving theifclause unconsumed, causingrescueto be seen as an unexpected token inbegin/rescueblocksdata |> sum |> puts—putsandprintnow accepted as bare pipe targets (previously raised an unexpected token error)
v1.3.0 (2025)
New Features
Language
- Default parameter values: def greet(name, msg="Hello", punct="!")
- Keyword-named parameters (e.g. times, each) now usable as variable/param names
- Triple-quoted multi-line strings: """...""" and '''...''' with interpolation
JSON (built-in json module — zero deps)
- json_parse(str) — parse JSON string → Frankie value
- json_dump(obj, pretty) — serialize to JSON string
- json_read(path) — read and parse JSON file
- json_write(path, obj, pretty) — serialize and write JSON file
CSV (built-in csv module — zero deps)
- csv_parse(text, headers) — parse CSV text → vector of hashes
- csv_dump(data, headers) — serialize vector of hashes → CSV string
- csv_read(path, headers) — read and parse CSV file
- csv_write(path, data, headers) — write CSV file
DateTime (built-in datetime module — zero deps)
- now() — current date and time
- today() — today's date at midnight
- date_from(year, month, day, hour, minute, second) — construct a date
- date_parse(str, fmt) — parse a date string (default fmt: %Y-%m-%d)
- .year, .month, .day, .hour, .minute, .second — accessors
- .format(fmt) — format with strftime directives
- .add_days(n), .add_hours(n), .add_minutes(n) — arithmetic
- .diff_days(other), .diff_seconds(other) — differences
- .weekday(), .weekday_name() — day of week
- .is_before(other), .is_after(other) — comparison
- .timestamp() — Unix timestamp
HTTP (built-in urllib — zero deps)
- http_get(url, headers) — GET request
- http_post(url, data, headers) — POST request (auto JSON-encodes dicts)
- http_put(url, data, headers) — PUT request
- http_delete(url, headers) — DELETE request
- Response: .status, .body, .headers, .json(), .ok()
- url_encode(hash) — encode params as query string
- url_decode(str) — decode query string → hash
Tooling
- frankiec new <project> — scaffold a new project with main.fk, test.fk, lib/, data/, README.md, .gitignore
- Better error messages — compile and runtime errors now show a boxed display with source context and line pointer (──▶)
- Syntax highlighting for VS Code (.tmLanguage.json + package.json + language-configuration.json)
- Syntax highlighting for Vim/Neovim (frankie.vim)
- Syntax highlighting for Sublime Text / TextMate (frankie.tmLanguage.json)
- All editor files in editors/
Bug Fixes
- Fixed
printoutput disappearing when running from a different working directory times,each,mapkeywords can now be used as parameter and variable names- Template placeholders in
frankiec newuse.replace()to avoidstr.format()key conflicts
v1.2.0 (2025)
New Features
Database Access (SQLite)
- db_open(path) — open or create a SQLite database; ":memory:" for in-memory
- db.exec(sql, params) — run DDL/DML with ? placeholders; returns row count
- db.query(sql, params) — SELECT → vector of hashes keyed by column name
- db.query_one(sql, params) — SELECT → first row as hash or nil
- db.insert(table, hash) — insert a hash of column→value; returns new row id
- db.insert_many(table, rows) — bulk insert a vector of hashes
- db.find_all(table) — all rows as vector of hashes
- db.find(table, where) — filtered rows (where is a hash, conditions ANDed)
- db.find_one(table, where) — first matching row or nil
- db.update(table, data, where) — update matching rows; returns count
- db.delete(table, where) — delete matching rows; returns count
- db.count(table) / db.count(table, where) — row counts
- db.last_id — rowid of last INSERT
- db.tables — list of table names in the database
- db.columns(table) — column info as vector of hashes
- db.transaction do...end — atomic block; rolls back on any error
- db.begin / db.commit / db.rollback — explicit transaction control
- db.close — close the connection
- Zero external dependencies — uses Python's built-in sqlite3
Multi-line Strings
- Triple-quoted strings """...""" and '''...''' spanning multiple lines
- String interpolation #{} works inside triple-double-quoted strings
- Perfect for embedding multi-line SQL, templates, or long text
Bug Fixes
isinstance(obj, FrankieDB)cross-namespace failure — fixed with duck typing (hasattrchecks instead) so DB objects work correctly insideexec()globalsdb.delete(table, where)was intercepted by string/hash delete handler — now correctly dispatches based on argument count (2 args = DB call).count("sub")on a DB object was routing to_fk_str_count— fixed via_fk_count_dispatchwith duck typing- Transaction
BEGIN/COMMIT/ROLLBACKnow uses explicitisolation_level=Nonewith an_in_txflag for correct per-operation autocommit and block rollback
v1.1.0 (2025)
New Features
Iterators & Collections
- .select do |x| — filter elements where block is true
- .reject do |x| — filter elements where block is false
- .reduce(init) do |acc, x| — fold to a single value (also .inject)
- .each_with_object(init) do |x, obj| — iterate with shared accumulator
- .any? do |x| — true if any element matches
- .all? do |x| — true if all elements match
- .none? do |x| — true if no elements match
- .count do |x| — count matching elements (or .count("sub") for strings)
- .flat_map do |x| — map then flatten one level
- .take(n) — first n elements
- .drop(n) — all but first n elements
- .tally — count occurrences → Hash
- .compact — remove nil values
- .chunk(n) — split into sub-vectors of size n
- .zip(other) — zip two vectors together
Control Flow
- case/when/else/end — pattern matching on values or conditions
- Bare case (no subject) — uses truthy when-clauses
Destructuring Assignment
- a, b, c = [1, 2, 3] — unpack vector into named variables
- Pads with nil if vector is shorter than target count
String Methods (new)
- .chars — vector of individual characters
- .bytes — vector of byte values
- .lines — vector of lines
- .chomp — remove trailing newline
- .chop — remove last character
- .count("sub") — count substring occurrences
- .center(w, pad) — center in field
- .ljust(w, pad) — left-justify in field
- .rjust(w, pad) — right-justify in field
- .squeeze — collapse consecutive duplicates
- .tr(from, to) — translate characters
- .each_char do |c| — iterate over characters
- .each_line do |l| — iterate over lines
- .lstrip / .rstrip — directional whitespace trim
REPL (Interactive Mode)
- frankiec repl — starts the interactive REPL
- frankiec with no arguments also launches the REPL
- Multi-line block detection — automatically waits for end
- vars — show all user-defined variables and functions
- clear — reset the session
- load <file.fk> — load a file into the current session
- help — show available commands
- Persistent state across expressions in a session
Bug Fixes
do...whilebody was accidentally consuming thewhilekeyword- Postfix
if/unlessnow works afterputs(not just expressions) matches()and all regex functions had flipped argument order — fixed to(string, pattern)s[-5..-1]negative range ends now parse as(-5)..(-1)correctly[x, x * 2]vector literal with multiplication was mis-parsed as destructuring — fixed with backtrackinggen_pipemethod lost itsdefheader during code insertion — restoredcountmethod now correctly dispatches:.count("sub")for strings,.count dofor filtering,.countfor length
Compiler Version
- Version header in generated files updated to v1.1
v1.0.0 (2025)
Initial Release
Core language
- 7 data types: Integer, Float, String, Boolean, Nil, Vector, Hash
- Full arithmetic: +, -, *, /, //, %, **
- String interpolation with #{}
- Ranges: 1..10 (inclusive), 1...10 (exclusive)
- Conditionals: if/elsif/else/end, unless/end
- Postfix if/unless
- Loops: while, until, do...while, for...in
- Iterators: .times, .each, .each_with_index, .map
- Functions with def...end and explicit return
- Named arguments: func(x, sep: "-")
- Pipe operator |>
- Destructuring (v1.1)
Collections - Vectors with R-style vectorized arithmetic - Hashes with symbol and integer keys, nil-safe access - Full method suites for both types
Standard Library
- Math: sqrt, abs, floor, ceil, min, max
- Statistics: sum, mean, median, stdev, variance
- Sequences: seq, linspace, rep, clamp
- String formatting: sprintf, paste
- Regex: matches, match, match_all, sub, gsub, =~, regex()
- File I/O: file_read, file_write, file_append, file_lines, file_exists, file_delete
- Type conversion: to_int, to_float, to_str
- Type checking: is_integer, is_float, is_string, is_vector, is_nil, is_bool
- System: exit, argv, env
Error handling
- begin/rescue/ensure/end
- raise
Multi-file
- require "filename" — load another .fk file once
Tooling
- frankiec run — run a program
- frankiec build — compile to Python source
- frankiec check — syntax check
- frankiec version
- python3 install.py — install frankiec to frankie/bin/