Examples

These are some longer examples than on the home page.

A complex loop

This example is intended to demonstrate how continue is translated, as well as breaking out of a function from within a loop.

import other.module (name1, name2)

def my_func(a: List[Integer], b: String) -> Value:
    x = 1
    y = 0
    for i in a + [5, 6, 7]:
        y += i
        x *= y
        def flip(x, y):
            return {x:y, y:x}

        if i == 5:
            continue
        {x, y} = flip(x, y)

        if x > 100:
            return x
    z = y + x
    return z

A search syntax

A language describing searching would typically allow arbitrarily nested parentheses, but that requires a feature, cyclic types, that won’t be immediately available. We can still describe and even evaluate fairly complex expressions, though.

# In a Union type, a tag, here "name" or "literal" determines what the variant stores.
type ValueExpr := Union:
    name ~ String
    literal ~ Integer
    zero~~

# The double tilde means the variant type is the "unit" type, which has only one value.
# That's how Tenet does enumerated types; true~~ and false~~ are Tenet's boolean values.

example_value_1 = name ~ "age"
example_value_2 = name ~ "count"
example_value_3 = literal ~ 55

# We use a Tuple to store possible arguments for a binary relation.
type BinaryRelation := Tuple:
    left: ValueExpr
    right: ValueExpr

# And another union distinguishes our types of search operators.
type SearchOperator := Union:
    equals ~ BinaryRelation
    not_equals ~ BinaryRelation
    positive ~ ValueExpr

type SearchExpression := List[SearchOperator]

example = [
    equals ~ {left: name ~ "age", right: literal ~ 55},
    positive ~ name ~ "count",
]

def evaluate(expr, names : Map[String, Integer]):
    result = true~~  # True and false are enumerated values.
    for op in expr:
        switch op
        case equals ~ {left, right}:
            result = result and eval_val(left) == eval_val(right)
        case not_equals ~ {left, right}:
            result = result and eval_val(left) != eval_val(right)
        case positive ~ val:
            result = result and eval_val(val) > 0
    return result

def eval_val(expr, names : Map[String, Integer]):
    switch expr
    case name ~ name:
        return names[name]
    case literal ~ num:
        return num
    case zero~~:
        return 0

A date-time library

This is a comprehensive example of Tenet code and shows what it can do even within current limitations.

# I wanted to put together an example tenet model based on an earlier project I had done
# that implemented an organizer. That, of couse, depends pretty heavily on dates and times,
# and I realized I'll need at least rudimentary support for it. Poking around, it seems
# like a naive (in that it doesn't handle timezones) implementation of iso8601 gets you
# 99% of what you need. It also stresses our ability to do math and such, even if the math
# is suprisingly easy. Those monks were some clever bastards!

# The two epochs here are posix, which most people are familiar with, and 'CE' meaning
# Jan 1 of year 1 of the common era. Actual dates are implemented according to the ISO 8601
# standard, which is the proleptic Gregorian calendar, except that years are astronomical,
# year +0001 corresponds with 1 AD, year 0000 corresponds with 1 BC, -0001 is 2 BC, etc.

# Timezone support would just be adding more fields.

# Define an epoch date; if we want to improve our date handling
# methods later, they can be backwards compatible.

type Iso8601Date = Tuple[year: Integer, month: Integer, day: Integer]
type Iso8601Time = Tuple[hour: Integer, minute: Integer, second: Integer]

type Duration = Union[seconds ~ Integer]

# type Iso8601Simple = Iso8601Date * Iso8601Time
type Iso8601Simple = Tuple:
    year: Integer
    month: Integer
    day: Integer
    hour: Integer
    minute: Integer
    second: Integer

type DateTime = Union:
    epoch_ce ~ Integer  # 0001-Jan-01 = 0
    epoch_posix ~ Integer  # 1970-Jan-01 = 0
    iso8601 ~ Iso8601Simple

#
# Datetime arithmetic
#

def dt_plus_dur(left: DateTime, right: Duration):
    dt_seconds = as_epoch_ce(left)
    dur_seconds = as_dur_seconds(right)
    return epoch_ce ~ (dt_seconds + dur_seconds)

def dt_minus_dur(left: DateTime, right: Duration):
    dt_seconds = as_epoch_ce(left)
    dur_seconds = as_dur_seconds(right)
    return epoch_ce ~ (dt_seconds - dur_seconds)

def dt_minus_dt(left:DateTime, right:DateTime):
    left_seconds = as_epoch_ce(left)
    right_seconds = as_epoch_ce(right)
    return dur_seconds ~ (left_seconds - right_seconds)

def dur_plus_dur(left: Duration, right: Duration):
    return dur_seconds ~ (as_dur_seconds(left) + as_dur_seconds(right))

#
# Conversion between forms.
#

def as_iso8601(value: DateTime):
    switch value
    case epoch_posix ~ epoch_seconds:
        return epoch_posix_to_iso8601(epoch_seconds)
    case epoch_ce ~ epoch_seconds:
        return epoch_ce_to_iso8601(seoncds)
    case iso8601 ~ datetime:
        return datetime

def as_epoch_ce(value: DateTime):
    switch value
    case epoch_posix ~ epoch_seconds:
        return epoch_posix_to_ce(epoch_seconds)
    case epoch_ce ~ epoch_seconds:
        return epoch_seconds
    case iso8601 ~ datetime:
        return iso8601_to_epoch_ce(datetime)

def as_epoch_posix(value: DateTime):
    switch value
    case epoch_posix ~ epoch_seconds:
        return epoch_seconds
    case epoch_ce ~ epoch_seconds:
        return epoch_ce_to_posix(epoch_seconds)
    case iso8601 ~ datetime:
        return iso8601_to_epoch_posix(datetime)

def as_dur_seconds(value: Duration):
    return value ? seconds

#
# Internal DateTime conversion functions
#

def epoch_ce_to_iso8601(epoch_seconds: Integer):
    {epoch_days::, day_seconds::} = split_epoch_seconds(epoch_seconds::)
    {year::, month::, day::} = epoch_days_to_year_month_day(epoch_days::)
    {hour::, minute::, second::} = seconds_to_hour_minute_second(day_seconds::)
    return {year::, month::, day::, hour::, minute::, second::}

def epoch_ce_to_posix(epoch_seconds: Integer):
    return epoch_seconds + ce_to_posix_offset

def epoch_posix_to_ce(epoch_seconds: Integer):
    return epoch_seconds - ce_to_posix_offset

def epoch_posix_to_iso8601(epoch_seconds: Integer):
    return epoch_ce_to_iso8601(epoch_posix_to_ce(epoch_seconds))

def iso8601_to_epoch_ce(datetime: Iso8601Simple):
    {year::, month::, day::, hour::, minute::, second::} = datetime
    epoch_days = year_month_day_to_epoch_days(year::, month::, day::)
    day_seconds = time_of_day_to_seconds(hour::, minute::, second::)
    return combine_epoch_date(epoch_days::, day_seconds::)

def iso8601_to_epoch_posix(datetime: Iso8601Simple):
    return epoch_ce_to_posix(iso8601_to_epoch_ce(datetime))

ce_to_posix_offset = 719162 * seconds_in_day

#
# Converting seconds from an epoch to epoch days and time of day
#

seconds_in_day = 24 * 60 * 60

def split_epoch_seconds(epoch_seconds: Integer):
    {div::, mod::} = div_mod(epoch_seconds, seconds_in_day)
    return {epoch_days: div, day_seconds: mod}

def split_epoch_ce(value: DateTime):
    return split_epoch_seconds(as_epoch_ce(value))

def combine_epoch_date(epoch_days: Integer, day_seconds: Integer):
    return seconds_in_day * epoch_days + day_seconds

def seconds_to_hour_minute_second(seconds: Integer):
    {div->minutes, mod->second} = div_mod(seconds, 60)
    {div->hour, mod->minute} = div_mod(minutes, 60)
    return {hour::, minute::, second::}

def hour_minute_second_to_seconds(hour:Integer, minute:Integer, second:Integer):
    return 3600 * hour + 60 * minute + second

#
# Converting days to y-m-d
#

# Note: year, month, day are all 0-based in calculations, but 1-based when exposed to the user.
# Epochs are at 0 seconds.

def month_start(month: Integer, is_leap_year: Boolean):
    if is_leap_year:
        starts = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]
    else:
        starts = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
    return starts[month]

days_in_quad_century = 146097
days_in_century = 36524
days_in_quad_year = 1461
days_in_std_year = 365

def epoch_day_to_year_month_day(epoch_days: Integer) -> Iso8601Date:
    {div->quad_century, mod->quad_centry_day} = div_mod(epoch_days, days_in_quad_century)

    # The first, second and third century have 36524,
    # the fourth has 36525.
    {div->century', mod->century_day} = div_mod(quad_century_day, days_in_century)

    # Thus, century' may be 0, 1, 2, 3 or 4.
    # 4 is only reported on the last day of a "leap century."
    if century' == 4:
        century = 3
    else:
        century = century'

    {div->quad_year, mod->quad_year_day} = div_mod(century_day, days_in_quad_year)
    # As before, year will be 4 on dec 31 of the leap year
    {div->year', mod->year_day} = div_mod(quad_year_day, days_in_year)
    if year' == 4:
        year = 3
    else:
        year = year'

    # Real day of the year, allowing Dec 31 on a leap year
    if century' == 4 or year' == 4:
        year_day = 365
    else:
        year_day = year_day'
    is_leap_year = century' == 4 or year' > 2 or quad_year == 24

    # This clever approximation came from python's datetime.
    month' = (year_day + 50) // 32

    # We then get when the estimated month starts...
    month_start_day = month_start(month:month', is_leap_year::)

    # ... which may be over by one
    if month_start_day > year_day:
        month = month' - 1
        month_start_day = month_start(month::, is_leap_year::)
    else:
        month = month'
    month_day = year_day - month_start_day

    return {year: 400 * quad_century + 100 * century + year + 1,
            month: month + 1, day: month_day + 1}

#
# Convert year-month-day to seconds after the epoch.
#

def year_month_day_to_epoch_days(year:Integer, month:Integer, day:Integer) -> Index:
    {div->quad_century, mod->quad_century_year} = div_mod(year - 1, 400)
    {div->century, mod->century_year} = div_mod(quad_century_year, 100)
    {div->quad_years, mod->quad_year} = div_mod(century_year, 4)
    is_leap_year = quad_year == 3 and (century_year != 99 or century == 3)
    month_start_day = month_start(month::, is_leap_year::)

    return (day - 1 +
            month_start_day +
            days_in_year * quad_year +
            days_in_quad_year * quad_years +
            days_in_century * century +
            days_in_quad_century * quad_century)

#
# Month name functions
#

type Month = Union[jan~~, feb~~, mar~~, apr~~, may~~, jun~~, jul~~, aug~~, sep~~, oct~~, nov~~, dec~~]

def month_from_index(index:Integer) -> Month:
    return [jan~~, feb~~, mar~~, apr~~, may~~, jun~~, jul~~, aug~~, sep~~, oct~~, nov~~, dec~~][index - 1]

def index_of_month(month: Month) -> Integer:
    return [
        jan~~: 1,
        feb~~: 2,
        mar~~: 3,
        apr~~: 4,
        may~~: 5,
        jun~~: 6,
        jul~~: 7,
        aug~~: 8,
        sep~~: 9,
        oct~~: 10,
        nov~~: 11,
        dec~~: 12,
    ][month]

#
# Days of the week and business day calculations
#

type DayOfWeek = Union[sun~~, mon~~, tue~~, wed~~, thu~~, fri~~, sat~~]

def day_of_week_from_index(index: Integer) -> DayOfWeek:
    return [sun~~, mon~~, tue~~, wed~~, thu~~, fri~~, sat~~][index]

def index_of_day_of_week(day: DayOfWeek) -> Integer:
    return [
        sun~~: 0,
        mon~~: 1,
        tue~~: 2,
        wed~~: 3,
        thu~~: 4,
        fri~~: 5,
        sat~~: 6,
    ][day]

# Get the index of the day of week, where Sunday is 0.

def day_of_week_of_epoch_days(epoch_days: Integer):
    # Jan 1, 0001 is a Saturday.
    return (6 + epoch_days) % 7

def day_of_week_date_time(value: DateTime):
    return day_of_week_of_epoch_days(split_epoch_ce(value).epoch_days)

def closest_business_day_forwards(value: Integer):
    switch day_of_week_of_epoch_days(epoch_days:value)
    case 0:
        return value + 1
    case 6:
        return value + 2
    else:
        return value

def closest_business_day_backwards(value: Integer):
    switch day_of_week_of_epoch_days(epoch_days:value)
    case 0:
        return value - 2
    case 6:
        return value - 1
    else:
        return value


# Increments a time by a number of business days, ensuring that the specific number of business days exist
# within the interval. Incrementing by 0 will force the time to the open of the next business day.
# open-seconds and close-seconds allow you to pick a window during which business is conducted, so it
# handles "lunch hours" reasonably well.

def business_days_after(point:DateTime, days_change:Integer, open_seconds:Integer, close_seconds:Integer):
    {epoch_days::, day_seconds::} = split_epoch_ce(point)
    after_hours = day_seconds > close_seconds
    before_hours = day_seconds < open_seconds
    if after_hours:
        day = 1 + epoch_days
    else:
        day = epoch_days
    if after_hours or before_hours:
        seconds' = open_seconds
    else:
        seconds' = day_seconds

    # Bump our answer to the next business day if we've landed on a weekend.
    day = closest_business_day_forwards(day)

    # Figure out how many weeks we're adding, and the remaining days
    {div->weeks_change, mod->days_change} = div_mod(days_change, 5)
    day += 7 * weeks_change + days_change

    # And now bump our answer to the next business day again.
    day = closest_business_day_forwards'(day)

    # And then combine the day and time.
    return epoch_ce ~ combine_epoch_date(epoch_days:day, day_seconds:seconds')

# Decrements a time by a number of business days, ensuring that the specific number of business days exist
# within the interval. Decrementing by 0 will force the time to the close of the previous business day.
# open-seconds and close-seconds allow you to pick a window during which business is conducted, so it
# handles "lunch hours" reasonably well.
def business_days_prior(point:DateTime, days_change:Integer, open_seconds:Integer, close_seconds:Integer):
    {epoch_days::, day_seconds::} = split_epoch_ce(point)

    after_hours = day_seconds > close_seconds
    before_hours = day_seconds < open_seconds
    if before_hours:
        day = epoch_days - 1
    else:
        day = epoch_days
    if after_hours or before_hours:
        seconds' = close_seconds
    else:
        seconds' = day_seconds

    # Bump our answer to the previous business day if we've landed on a weekend.
    day = closest_business_day_backwards(day)

    # Figure out how many weeks we're subtracting, and the remaining days
    {div->weeks_change, mod->days_change} = div_mod(days_change, 5)
    day += 7 * weeks_change + days_change

    # And now bump our answer to the previous business day again.
    day = closest_business_day_backwards(day)

    # And then combine the day and time.
    return epoch_ce ~ combine_epoch_date(epoch_days:day, day_seconds:seconds')