feat(stdlib): Time/Date/Duration calendar-clock core (issue #9)
Some checks are pending
docs / build-and-deploy (push) Waiting to run

Implement the calendar/clock half of #9 as plain-i32 integer epochs — no
new type, no floating point (the issue's "integer epochs to avoid drift") —
so every operation is deterministic and bit-identical on every platform:

  Duration — a span in whole seconds; seconds/minutes/hours/days build one,
             as_seconds/as_minutes/as_hours/as_days read it back. Because a
             duration is just an int, `+` and `>` work with no extra machinery
             (Duration.minutes(5) + Duration.seconds(30), away > Duration.hours(3)).
  Date     — a civil day as days-since-1970 (UTC): new/year/month/day/weekday/
             is_leap/days_in_month/to_epoch/add_days/diff_days.
  DateTime — an instant as seconds-since-1970 (UTC, matching Time.now):
             from/date/add/year/month/day/weekday/hour/minute/second.
  Time.since(past) = now - past, for offline-progress / "time away" checks.

New selfhost/emit_datetime.ludic (is_/emit_ for the three namespaces, wired
into emit_ns_call + the frag list). The two civil<->epoch conversions are
Howard Hinnant's public-domain proleptic-Gregorian algorithms, emitted once
per program as the @fn_days_from_civil / @fn_civil_from_days prelude and gated
by g_uses_datert; days_in_month is next-month-day-0 (no lookup table). Time
gains `since`. Docs (Duration/Date/DateTime sections, 28 method pages +
time-since), inventory, and LSP hover kept in sync; a registered test checks
component math against hand-computed values. Reseeded; C-free fixpoint holds;
all suites green (26 self-host / 45 regression / 29 tools); check.py,
check-impl.py and validate.py OK.

format/parse, a game-controlled simulated clock, and timezones are tracked
follow-ups; v1 is UTC-only and, on the i32 epoch, valid through 2038.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 03:37:38 +03:00
parent 121053e179
commit 1a2c6ec2c7
43 changed files with 10670 additions and 7626 deletions

View file

@ -0,0 +1,7 @@
---
id: date
title: Date
order: 6
---
Calendar days on the proleptic Gregorian calendar (UTC). A <code>Date</code> is stored as a plain <code>int</code>: the count of days since 1970-01-01, so shifting a date by whole days is ordinary integer arithmetic and two dates subtract to a day count. Build one with <code>Date.new</code>, read its parts with <code>year</code>/<code>month</code>/<code>day</code>/<code>weekday</code>. Every operation is deterministic integer math — no leap seconds, no timezone, no floating point.

View file

@ -0,0 +1,22 @@
---
id: date-add_days
name: Date.add_days
category: date
kind: namespace-method
tokens: Date.add_days
sig: Date.add_days(date, n) -> int
tip: The date n days later (n may be negative).
order: 8
ns: Date
member: add_days
---
The <code>Date</code> that is <code>n</code> days after <code>date</code>; pass a negative <code>n</code> to go backward. Rolls across month and year boundaries automatically.
```ludic
program Demo {
handler Step phase Update {
let tomorrow = Date.add_days(Date.new(2026, 12, 31), 1) # 2027-01-01
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-day
name: Date.day
category: date
kind: namespace-method
tokens: Date.day
sig: Date.day(date) -> int
tip: The day of the month (1-31) of a date.
order: 3
ns: Date
member: day
---
The day of the month of a <code>Date</code>, from <code>1</code> to <code>31</code>.
```ludic
program Demo {
handler Step phase Update {
let d = Date.day(Date.new(2026, 8, 30)) # 30
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-days_in_month
name: Date.days_in_month
category: date
kind: namespace-method
tokens: Date.days_in_month
sig: Date.days_in_month(year, month) -> int
tip: Number of days in a given month.
order: 6
ns: Date
member: days_in_month
---
The number of days in <code>month</code> of <code>year</code> (28&ndash;31), accounting for leap Februaries.
```ludic
program Demo {
handler Step phase Update {
let n = Date.days_in_month(2024, 2) # 29
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-diff_days
name: Date.diff_days
category: date
kind: namespace-method
tokens: Date.diff_days
sig: Date.diff_days(a, b) -> int
tip: Whole days from b to a (a - b).
order: 9
ns: Date
member: diff_days
---
The number of whole days from <code>b</code> to <code>a</code> (that is, <code>a - b</code>). Negative when <code>a</code> precedes <code>b</code>.
```ludic
program Demo {
handler Step phase Update {
let gap = Date.diff_days(Date.new(2026, 1, 1), Date.new(2025, 1, 1)) # 365
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-is_leap
name: Date.is_leap
category: date
kind: namespace-method
tokens: Date.is_leap
sig: Date.is_leap(year) -> bool
tip: True if the year is a leap year.
order: 5
ns: Date
member: is_leap
---
Whether <code>year</code> is a leap year on the proleptic Gregorian calendar: divisible by 4, except centuries, which must be divisible by 400.
```ludic
program Demo {
handler Step phase Update {
let leap = Date.is_leap(2024) # true
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-month
name: Date.month
category: date
kind: namespace-method
tokens: Date.month
sig: Date.month(date) -> int
tip: The month (1-12) of a date.
order: 2
ns: Date
member: month
---
The calendar month of a <code>Date</code>, from <code>1</code> (January) to <code>12</code> (December).
```ludic
program Demo {
handler Step phase Update {
let m = Date.month(Date.new(2026, 8, 30)) # 8
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-new
name: Date.new
category: date
kind: namespace-method
tokens: Date.new
sig: Date.new(year, month, day) -> int
tip: A calendar day as days-since-1970.
order: 0
ns: Date
member: new
---
Builds a <code>Date</code> from a calendar <code>year</code>, <code>month</code> (1&ndash;12) and <code>day</code> (1&ndash;31), returning the count of days since 1970-01-01 (UTC). Read the parts back with <code>Date.year</code>/<code>Date.month</code>/<code>Date.day</code>.
```ludic
program Demo {
handler Step phase Update {
let launch = Date.new(2026, 8, 30)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-to_epoch
name: Date.to_epoch
category: date
kind: namespace-method
tokens: Date.to_epoch
sig: Date.to_epoch(date) -> int
tip: Midnight UTC of the day, as a DateTime.
order: 7
ns: Date
member: to_epoch
---
The instant at midnight (00:00:00 UTC) of the given <code>Date</code>, as a <code>DateTime</code> (seconds since 1970). Bridges a calendar day back to the wall-clock timeline.
```ludic
program Demo {
handler Step phase Update {
let t = Date.to_epoch(Date.new(2026, 8, 30))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-weekday
name: Date.weekday
category: date
kind: namespace-method
tokens: Date.weekday
sig: Date.weekday(date) -> int
tip: Day of the week, 0=Sunday..6=Saturday.
order: 4
ns: Date
member: weekday
---
The day of the week of a <code>Date</code>, as <code>0</code> (Sunday) through <code>6</code> (Saturday). Useful for weekly events or daily-reward streaks.
```ludic
program Demo {
handler Step phase Update {
let w = Date.weekday(Date.new(2000, 1, 1)) # 6 (Saturday)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: date-year
name: Date.year
category: date
kind: namespace-method
tokens: Date.year
sig: Date.year(date) -> int
tip: The calendar year of a date.
order: 1
ns: Date
member: year
---
The calendar year of a <code>Date</code>.
```ludic
program Demo {
handler Step phase Update {
let y = Date.year(Date.new(2026, 8, 30)) # 2026
}
}
```

View file

@ -0,0 +1,7 @@
---
id: datetime
title: DateTime
order: 6
---
Instants on the wall-clock timeline (UTC), stored as a plain <code>int</code>: the count of seconds since 1970-01-01, matching <code>Time.now</code>. Build one with <code>DateTime.from</code>, shift it by a <code>Duration</code> with <code>DateTime.add</code>, and pull it apart with the <code>year</code>/<code>month</code>/<code>day</code>/<code>hour</code>/<code>minute</code>/<code>second</code> readers. Deterministic integer math throughout; v1 is UTC-only with no leap seconds, and assumes instants at or after 1970.

View file

@ -0,0 +1,22 @@
---
id: datetime-add
name: DateTime.add
category: datetime
kind: namespace-method
tokens: DateTime.add
sig: DateTime.add(dt, span) -> int
tip: The instant `span` seconds later.
order: 2
ns: DateTime
member: add
---
The instant <code>span</code> seconds after <code>dt</code>. Pair it with the <code>Duration</code> constructors to express the offset in real units.
```ludic
program Demo {
handler Step phase Update {
let ready_at = DateTime.add(Time.now(), Duration.hours(3))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-date
name: DateTime.date
category: datetime
kind: namespace-method
tokens: DateTime.date
sig: DateTime.date(dt) -> int
tip: The calendar day the instant falls on.
order: 1
ns: DateTime
member: date
---
The <code>Date</code> (days since 1970) that the instant <code>dt</code> falls on, dropping the time of day.
```ludic
program Demo {
handler Step phase Update {
let today = DateTime.date(Time.now())
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-day
name: DateTime.day
category: datetime
kind: namespace-method
tokens: DateTime.day
sig: DateTime.day(dt) -> int
tip: The day of the month (1-31) of an instant.
order: 5
ns: DateTime
member: day
---
The day of the month of the instant <code>dt</code> (UTC), <code>1</code>&ndash;<code>31</code>.
```ludic
program Demo {
handler Step phase Update {
let d = DateTime.day(DateTime.from(2000, 3, 9, 0, 0, 0)) # 9
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-from
name: DateTime.from
category: datetime
kind: namespace-method
tokens: DateTime.from
sig: DateTime.from(year, month, day, hour, minute, second) -> int
tip: Build an instant from its calendar parts.
order: 0
ns: DateTime
member: from
---
Builds a <code>DateTime</code> (seconds since 1970, UTC) from calendar and clock parts. The inverse of the component readers below.
```ludic
program Demo {
handler Step phase Update {
let t = DateTime.from(2000, 1, 1, 12, 30, 15)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-hour
name: DateTime.hour
category: datetime
kind: namespace-method
tokens: DateTime.hour
sig: DateTime.hour(dt) -> int
tip: The hour of day (0-23) of an instant.
order: 7
ns: DateTime
member: hour
---
The hour of the day of the instant <code>dt</code> (UTC), <code>0</code>&ndash;<code>23</code>. Drives day/night cycles from a real clock.
```ludic
program Demo {
handler Step phase Update {
let h = DateTime.hour(DateTime.from(2000, 1, 1, 12, 30, 15)) # 12
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-minute
name: DateTime.minute
category: datetime
kind: namespace-method
tokens: DateTime.minute
sig: DateTime.minute(dt) -> int
tip: The minute of the hour (0-59) of an instant.
order: 8
ns: DateTime
member: minute
---
The minute within the hour of the instant <code>dt</code> (UTC), <code>0</code>&ndash;<code>59</code>.
```ludic
program Demo {
handler Step phase Update {
let mi = DateTime.minute(DateTime.from(2000, 1, 1, 12, 30, 15)) # 30
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-month
name: DateTime.month
category: datetime
kind: namespace-method
tokens: DateTime.month
sig: DateTime.month(dt) -> int
tip: The month (1-12) of an instant.
order: 4
ns: DateTime
member: month
---
The calendar month of the instant <code>dt</code> (UTC), <code>1</code>&ndash;<code>12</code>.
```ludic
program Demo {
handler Step phase Update {
let m = DateTime.month(DateTime.from(2000, 3, 9, 0, 0, 0)) # 3
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-second
name: DateTime.second
category: datetime
kind: namespace-method
tokens: DateTime.second
sig: DateTime.second(dt) -> int
tip: The second of the minute (0-59) of an instant.
order: 9
ns: DateTime
member: second
---
The second within the minute of the instant <code>dt</code> (UTC), <code>0</code>&ndash;<code>59</code>.
```ludic
program Demo {
handler Step phase Update {
let s = DateTime.second(DateTime.from(2000, 1, 1, 12, 30, 15)) # 15
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-weekday
name: DateTime.weekday
category: datetime
kind: namespace-method
tokens: DateTime.weekday
sig: DateTime.weekday(dt) -> int
tip: Day of the week, 0=Sunday..6=Saturday.
order: 6
ns: DateTime
member: weekday
---
The day of the week of the instant <code>dt</code> (UTC), <code>0</code> (Sunday) through <code>6</code> (Saturday).
```ludic
program Demo {
handler Step phase Update {
let w = DateTime.weekday(DateTime.from(2000, 1, 1, 12, 0, 0)) # 6
}
}
```

View file

@ -0,0 +1,22 @@
---
id: datetime-year
name: DateTime.year
category: datetime
kind: namespace-method
tokens: DateTime.year
sig: DateTime.year(dt) -> int
tip: The calendar year of an instant.
order: 3
ns: DateTime
member: year
---
The calendar year of the instant <code>dt</code> (UTC).
```ludic
program Demo {
handler Step phase Update {
let y = DateTime.year(DateTime.from(2000, 1, 1, 12, 30, 15)) # 2000
}
}
```

View file

@ -0,0 +1,7 @@
---
id: duration
title: Duration
order: 6
---
Spans of real time, measured in whole seconds and carried in a plain <code>int</code> — so a duration adds, subtracts and compares with the ordinary operators (<code>Duration.minutes(5) + Duration.seconds(30)</code>, <code>away &gt; Duration.hours(3)</code>). The constructors build a span from a unit; the <code>as_*</code> readers convert a span back to whole units (truncating toward zero). Integer-only, so no floating-point drift.

View file

@ -0,0 +1,22 @@
---
id: duration-as_days
name: Duration.as_days
category: duration
kind: namespace-method
tokens: Duration.as_days
sig: Duration.as_days(span) -> int
tip: The span in whole days.
order: 7
ns: Duration
member: as_days
---
The span expressed in whole days, truncated toward zero (<code>span / 86400</code>).
```ludic
program Demo {
handler Step phase Update {
let d = Duration.as_days(Duration.hours(50)) # 2
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-as_hours
name: Duration.as_hours
category: duration
kind: namespace-method
tokens: Duration.as_hours
sig: Duration.as_hours(span) -> int
tip: The span in whole hours.
order: 6
ns: Duration
member: as_hours
---
The span expressed in whole hours, truncated toward zero (<code>span / 3600</code>). Useful for turning an elapsed-time gap into a reward count.
```ludic
program Demo {
handler Step phase Update {
let h = Duration.as_hours(Duration.days(3)) # 72
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-as_minutes
name: Duration.as_minutes
category: duration
kind: namespace-method
tokens: Duration.as_minutes
sig: Duration.as_minutes(span) -> int
tip: The span in whole minutes.
order: 5
ns: Duration
member: as_minutes
---
The span expressed in whole minutes, truncated toward zero (<code>span / 60</code>).
```ludic
program Demo {
handler Step phase Update {
let m = Duration.as_minutes(Duration.hours(2)) # 120
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-as_seconds
name: Duration.as_seconds
category: duration
kind: namespace-method
tokens: Duration.as_seconds
sig: Duration.as_seconds(span) -> int
tip: The span in whole seconds.
order: 4
ns: Duration
member: as_seconds
---
The span expressed in whole seconds. The inverse of <code>Duration.seconds</code>; also the identity, provided for symmetry with the other readers.
```ludic
program Demo {
handler Step phase Update {
let s = Duration.as_seconds(Duration.minutes(3)) # 180
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-days
name: Duration.days
category: duration
kind: namespace-method
tokens: Duration.days
sig: Duration.days(n) -> int
tip: A span of n days, in seconds.
order: 3
ns: Duration
member: days
---
A span of <code>n</code> days, returned in seconds (<code>n * 86400</code>).
```ludic
program Demo {
handler Step phase Update {
let week = Duration.days(7)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-hours
name: Duration.hours
category: duration
kind: namespace-method
tokens: Duration.hours
sig: Duration.hours(n) -> int
tip: A span of n hours, in seconds.
order: 2
ns: Duration
member: hours
---
A span of <code>n</code> hours, returned in seconds (<code>n * 3600</code>). Handy as a threshold for offline-progress checks.
```ludic
program Demo {
handler Step phase Update {
let full_day = Duration.hours(24)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-minutes
name: Duration.minutes
category: duration
kind: namespace-method
tokens: Duration.minutes
sig: Duration.minutes(n) -> int
tip: A span of n minutes, in seconds.
order: 1
ns: Duration
member: minutes
---
A span of <code>n</code> minutes, returned in seconds (<code>n * 60</code>).
```ludic
program Demo {
handler Step phase Update {
let respawn = Duration.minutes(2)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: duration-seconds
name: Duration.seconds
category: duration
kind: namespace-method
tokens: Duration.seconds
sig: Duration.seconds(n) -> int
tip: A span of n whole seconds.
order: 0
ns: Duration
member: seconds
---
A span of <code>n</code> whole seconds. Since a duration is just an <code>int</code> count of seconds, this is the identity — it exists so the unit is explicit at the call site.
```ludic
program Demo {
handler Step phase Update {
let cooldown = Duration.seconds(45)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: time-since
name: Time.since
category: time
kind: namespace-method
tokens: Time.since
sig: Time.since(past) -> int
tip: Seconds elapsed from a past instant to now (non-deterministic).
order: 4
ns: Time
member: since
---
The number of seconds elapsed from the <code>DateTime</code> <code>past</code> to now (<code>Time.now() - past</code>). Because it reads the wall clock it is <b>non-deterministic</b> &mdash; use it for offline-progress and "time away" checks, never inside the lockstep simulation. The result is a <code>Duration</code> in seconds, ready for the <code>Duration.as_*</code> readers.
```ludic
program Demo {
handler Step phase Update {
let last_seen = DateTime.from(2026, 8, 30, 9, 0, 0)
let away = Time.since(last_seen)
let hours_away = Duration.as_hours(away)
}
}
```

View file

@ -25,6 +25,7 @@ var g_uses_mathrt: bool = false # Math.sqrt/sin/cos/tan was emitted -> emit the
var g_uses_textrt: bool = false # Text.upper/lower/trim/repeat/pad was emitted -> emit the text builders var g_uses_textrt: bool = false # Text.upper/lower/trim/repeat/pad was emitted -> emit the text builders
var g_uses_textrt2: bool = false # Text.split/join/replace was emitted -> emit the string/slice builders var g_uses_textrt2: bool = false # Text.split/join/replace was emitted -> emit the string/slice builders
var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
# loop targets for break/continue (innermost last) # loop targets for break/continue (innermost last)

View file

@ -0,0 +1,262 @@
# emit_datetime.ludic — the calendar/clock half of the stdlib: the Duration.*,
# Date.* and DateTime.* namespaces (issue #9). Everything is a plain i32 integer
# epoch, never a float, so it is deterministic and bit-identical on every
# platform:
# Duration — a span in whole seconds (Duration.hours(3) == 10800).
# Date — a civil day, stored as the count of days since 1970-01-01 (UTC),
# so Date arithmetic is ordinary integer add/subtract.
# DateTime — an instant, stored as the count of seconds since the 1970 epoch
# (UTC), matching Time.now().
# The two civil<->epoch conversions are Howard Hinnant's public-domain algorithms
# (chrono-compatible, proleptic Gregorian), emitted once per program as the
# @fn_days_from_civil / @fn_civil_from_days prelude and gated by g_uses_datert.
# v1 is UTC-only with no leap seconds; instants are assumed non-negative (dates
# at or after 1970). Timezones, format/parse and a game-controlled simulated
# clock are tracked follow-ups.
# --- Duration.* — spans in whole seconds -------------------------------------
function is_duration_ns(meth: pointer) -> bool {
if (meth == "seconds") or (meth == "minutes") or (meth == "hours") or (meth == "days") { return true }
if (meth == "as_seconds") or (meth == "as_minutes") or (meth == "as_hours") or (meth == "as_days") { return true }
return false
}
function emit_duration_ns(meth: pointer, e: Node) -> Val {
let n = emit_expr(e.kids[0])
if (meth == "seconds") { return val(n.code, "int") } # already seconds
if (meth == "minutes") { return val(emit_bind(`mul i32 {n.code}, 60`), "int") }
if (meth == "hours") { return val(emit_bind(`mul i32 {n.code}, 3600`), "int") }
if (meth == "days") { return val(emit_bind(`mul i32 {n.code}, 86400`), "int") }
if (meth == "as_seconds") { return val(n.code, "int") }
if (meth == "as_minutes") { return val(emit_bind(`sdiv i32 {n.code}, 60`), "int") }
if (meth == "as_hours") { return val(emit_bind(`sdiv i32 {n.code}, 3600`), "int") }
# as_days: whole days in the span (floor toward zero)
return val(emit_bind(`sdiv i32 {n.code}, 86400`), "int")
}
# --- Date.* — a civil day as days-since-1970 ---------------------------------
# load one component (0=year, 1=month, 2=day) of the civil date for epoch-day
# code `ed` by calling the civil_from_days prelude into three stack slots.
function date_component(ed: pointer, which: int) -> pointer {
g_uses_datert = true
let yp = emit_alloca("i32")
let mp = emit_alloca("i32")
let dp = emit_alloca("i32")
emit(` call void @fn_civil_from_days(i32 {ed}, ptr {yp}, ptr {mp}, ptr {dp})\n`)
var p = yp
if (which == 1) { p = mp }
if (which == 2) { p = dp }
return emit_bind(`load i32, ptr {p}`)
}
function is_date_ns(meth: pointer) -> bool {
if (meth == "new") or (meth == "year") or (meth == "month") or (meth == "day") { return true }
if (meth == "weekday") or (meth == "is_leap") or (meth == "days_in_month") { return true }
if (meth == "to_epoch") or (meth == "add_days") or (meth == "diff_days") { return true }
return false
}
function emit_date_ns(meth: pointer, e: Node) -> Val {
if (meth == "new") { # new(year, month, day) -> Date (epoch-day)
g_uses_datert = true
let y = emit_expr(e.kids[0]); let m = emit_expr(e.kids[1]); let d = emit_expr(e.kids[2])
return val(emit_bind(`call i32 @fn_days_from_civil(i32 {y.code}, i32 {m.code}, i32 {d.code})`), "int")
}
if (meth == "year") { let ed = emit_expr(e.kids[0]); return val(date_component(ed.code, 0), "int") }
if (meth == "month") { let ed = emit_expr(e.kids[0]); return val(date_component(ed.code, 1), "int") }
if (meth == "day") { let ed = emit_expr(e.kids[0]); return val(date_component(ed.code, 2), "int") }
if (meth == "weekday") { # 0=Sunday .. 6=Saturday (1970-01-01 was Thursday)
let ed = emit_expr(e.kids[0])
let s = emit_bind(`add i32 {ed.code}, 4`) # shift so the epoch's Thursday lands right
let r = emit_bind(`srem i32 {s}, 7`)
let rr = emit_bind(`add i32 {r}, 7`) # normalise a negative remainder into [0,7)
return val(emit_bind(`srem i32 {rr}, 7`), "int")
}
if (meth == "is_leap") { # proleptic Gregorian leap-year test
let y = emit_expr(e.kids[0])
let m4 = emit_bind(`srem i32 {y.code}, 4`); let c4 = emit_bind(`icmp eq i32 {m4}, 0`)
let m100 = emit_bind(`srem i32 {y.code}, 100`); let c100 = emit_bind(`icmp ne i32 {m100}, 0`)
let m400 = emit_bind(`srem i32 {y.code}, 400`); let c400 = emit_bind(`icmp eq i32 {m400}, 0`)
let common = emit_bind(`and i1 {c4}, {c100}`) # divisible by 4 but not by 100
let leap = emit_bind(`or i1 {common}, {c400}`) # ...or divisible by 400
return val(emit_bind(`zext i1 {leap} to i32`), "bool")
}
if (meth == "days_in_month") { # length of (year, month) = next month's day 0
g_uses_datert = true
let y = emit_expr(e.kids[0]); let m = emit_expr(e.kids[1])
let this = emit_bind(`call i32 @fn_days_from_civil(i32 {y.code}, i32 {m.code}, i32 1)`)
let dec = emit_bind(`icmp eq i32 {m.code}, 12`) # December rolls over to next January
let ny = emit_bind(`add i32 {y.code}, 1`)
let ny2 = emit_bind(`select i1 {dec}, i32 {ny}, i32 {y.code}`)
let nm = emit_bind(`add i32 {m.code}, 1`)
let nm2 = emit_bind(`select i1 {dec}, i32 1, i32 {nm}`)
let next = emit_bind(`call i32 @fn_days_from_civil(i32 {ny2}, i32 {nm2}, i32 1)`)
return val(emit_bind(`sub i32 {next}, {this}`), "int")
}
if (meth == "to_epoch") { # midnight UTC of the day, as a DateTime instant
let ed = emit_expr(e.kids[0])
return val(emit_bind(`mul i32 {ed.code}, 86400`), "int")
}
if (meth == "add_days") { # the day `n` days after `ed`
let ed = emit_expr(e.kids[0]); let n = emit_expr(e.kids[1])
return val(emit_bind(`add i32 {ed.code}, {n.code}`), "int")
}
# diff_days(a, b) -> whole days from b to a (a - b)
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
return val(emit_bind(`sub i32 {a.code}, {b.code}`), "int")
}
# --- DateTime.* — an instant as seconds-since-1970 ---------------------------
# seconds-of-day for instant `t`, normalised into [0, 86400) even for a negative
# instant, so hour/minute/second stay correct.
function dt_secofday(t: pointer) -> pointer {
let r = emit_bind(`srem i32 {t}, 86400`)
let rr = emit_bind(`add i32 {r}, 86400`)
return emit_bind(`srem i32 {rr}, 86400`)
}
# the epoch-day (floored) an instant falls on: floor(t / 86400).
function dt_epochday(t: pointer) -> pointer {
let sod = dt_secofday(t) # t - sod is an exact multiple of 86400
let base = emit_bind(`sub i32 {t}, {sod}`)
return emit_bind(`sdiv i32 {base}, 86400`)
}
function is_datetime_ns(meth: pointer) -> bool {
if (meth == "from") or (meth == "date") or (meth == "add") { return true }
if (meth == "year") or (meth == "month") or (meth == "day") or (meth == "weekday") { return true }
if (meth == "hour") or (meth == "minute") or (meth == "second") { return true }
return false
}
function emit_datetime_ns(meth: pointer, e: Node) -> Val {
if (meth == "from") { # from(year, month, day, hour, minute, second) -> DateTime
g_uses_datert = true
let y = emit_expr(e.kids[0]); let mo = emit_expr(e.kids[1]); let d = emit_expr(e.kids[2])
let h = emit_expr(e.kids[3]); let mi = emit_expr(e.kids[4]); let s = emit_expr(e.kids[5])
let ed = emit_bind(`call i32 @fn_days_from_civil(i32 {y.code}, i32 {mo.code}, i32 {d.code})`)
let days = emit_bind(`mul i32 {ed}, 86400`)
let hh = emit_bind(`mul i32 {h.code}, 3600`)
let mm = emit_bind(`mul i32 {mi.code}, 60`)
let a = emit_bind(`add i32 {days}, {hh}`)
let b = emit_bind(`add i32 {a}, {mm}`)
return val(emit_bind(`add i32 {b}, {s.code}`), "int")
}
if (meth == "date") { # the calendar day this instant is on -> Date
let t = emit_expr(e.kids[0])
return val(dt_epochday(t.code), "int")
}
if (meth == "add") { # the instant `dur` seconds after `t`
let t = emit_expr(e.kids[0]); let dur = emit_expr(e.kids[1])
return val(emit_bind(`add i32 {t.code}, {dur.code}`), "int")
}
if (meth == "year") { let t = emit_expr(e.kids[0]); return val(date_component(dt_epochday(t.code), 0), "int") }
if (meth == "month") { let t = emit_expr(e.kids[0]); return val(date_component(dt_epochday(t.code), 1), "int") }
if (meth == "day") { let t = emit_expr(e.kids[0]); return val(date_component(dt_epochday(t.code), 2), "int") }
if (meth == "weekday") { # 0=Sunday .. 6=Saturday
let t = emit_expr(e.kids[0])
let ed = dt_epochday(t.code)
let s = emit_bind(`add i32 {ed}, 4`)
let r = emit_bind(`srem i32 {s}, 7`)
let rr = emit_bind(`add i32 {r}, 7`)
return val(emit_bind(`srem i32 {rr}, 7`), "int")
}
if (meth == "hour") { # 0..23
let t = emit_expr(e.kids[0])
let sod = dt_secofday(t.code)
return val(emit_bind(`sdiv i32 {sod}, 3600`), "int")
}
if (meth == "minute") { # 0..59
let t = emit_expr(e.kids[0])
let sod = dt_secofday(t.code)
let h = emit_bind(`srem i32 {sod}, 3600`)
return val(emit_bind(`sdiv i32 {h}, 60`), "int")
}
# second: 0..59
let t = emit_expr(e.kids[0])
let sod = dt_secofday(t.code)
return val(emit_bind(`srem i32 {sod}, 60`), "int")
}
# emit_datetime_prelude — the two civil<->epoch conversions, emitted once per
# program that uses Date/DateTime (g_uses_datert). Both are Howard Hinnant's
# public-domain proleptic-Gregorian algorithms, in pure i32 integer IR, so they
# are deterministic and bit-identical everywhere. 719468 is the day count from
# 0000-03-01 to 1970-01-01; 146097 is the days in a 400-year era.
function emit_datetime_prelude() -> void {
# @fn_days_from_civil(y, m, d) -> days since 1970-01-01
emith("define i32 @fn_days_from_civil(i32 %y0, i32 %m, i32 %d) {\n")
emith(" %mle2 = icmp sle i32 %m, 2\n")
emith(" %ysub = select i1 %mle2, i32 1, i32 0\n")
emith(" %y = sub i32 %y0, %ysub\n")
emith(" %yneg = icmp slt i32 %y, 0\n")
emith(" %ym399 = sub i32 %y, 399\n")
emith(" %enum = select i1 %yneg, i32 %ym399, i32 %y\n")
emith(" %era = sdiv i32 %enum, 400\n")
emith(" %era400 = mul i32 %era, 400\n")
emith(" %yoe = sub i32 %y, %era400\n")
emith(" %mgt2 = icmp sgt i32 %m, 2\n")
emith(" %mshift = select i1 %mgt2, i32 -3, i32 9\n")
emith(" %mm = add i32 %m, %mshift\n")
emith(" %t153 = mul i32 153, %mm\n")
emith(" %t153b = add i32 %t153, 2\n")
emith(" %doy0 = sdiv i32 %t153b, 5\n")
emith(" %doy1 = add i32 %doy0, %d\n")
emith(" %doy = sub i32 %doy1, 1\n")
emith(" %yoe365 = mul i32 %yoe, 365\n")
emith(" %yoe4 = sdiv i32 %yoe, 4\n")
emith(" %yoe100 = sdiv i32 %yoe, 100\n")
emith(" %doe0 = add i32 %yoe365, %yoe4\n")
emith(" %doe1 = sub i32 %doe0, %yoe100\n")
emith(" %doe = add i32 %doe1, %doy\n")
emith(" %e146097 = mul i32 %era, 146097\n")
emith(" %r0 = add i32 %e146097, %doe\n")
emith(" %r = sub i32 %r0, 719468\n")
emith(" ret i32 %r\n")
emith("}\n")
# @fn_civil_from_days(z0, yp, mp, dp): write the civil (year, month, day) of
# the epoch-day z0 through the three out-pointers.
emith("define void @fn_civil_from_days(i32 %z0, ptr %yp, ptr %mp, ptr %dp) {\n")
emith(" %z = add i32 %z0, 719468\n")
emith(" %zneg = icmp slt i32 %z, 0\n")
emith(" %zm = sub i32 %z, 146096\n")
emith(" %enum = select i1 %zneg, i32 %zm, i32 %z\n")
emith(" %era = sdiv i32 %enum, 146097\n")
emith(" %era146097 = mul i32 %era, 146097\n")
emith(" %doe = sub i32 %z, %era146097\n")
emith(" %d1460 = sdiv i32 %doe, 1460\n")
emith(" %d36524 = sdiv i32 %doe, 36524\n")
emith(" %d146096 = sdiv i32 %doe, 146096\n")
emith(" %ya = sub i32 %doe, %d1460\n")
emith(" %yb = add i32 %ya, %d36524\n")
emith(" %yc = sub i32 %yb, %d146096\n")
emith(" %yoe = sdiv i32 %yc, 365\n")
emith(" %era400 = mul i32 %era, 400\n")
emith(" %yy = add i32 %yoe, %era400\n")
emith(" %yoe365 = mul i32 %yoe, 365\n")
emith(" %yoe4 = sdiv i32 %yoe, 4\n")
emith(" %yoe100 = sdiv i32 %yoe, 100\n")
emith(" %sub0 = add i32 %yoe365, %yoe4\n")
emith(" %sub1 = sub i32 %sub0, %yoe100\n")
emith(" %doy = sub i32 %doe, %sub1\n")
emith(" %fivedoy = mul i32 5, %doy\n")
emith(" %fivedoy2 = add i32 %fivedoy, 2\n")
emith(" %mpv = sdiv i32 %fivedoy2, 153\n")
emith(" %m153 = mul i32 153, %mpv\n")
emith(" %m153b = add i32 %m153, 2\n")
emith(" %m153c = sdiv i32 %m153b, 5\n")
emith(" %dd0 = sub i32 %doy, %m153c\n")
emith(" %dd = add i32 %dd0, 1\n")
emith(" %mplt10 = icmp slt i32 %mpv, 10\n")
emith(" %mplus3 = add i32 %mpv, 3\n")
emith(" %mminus9 = sub i32 %mpv, 9\n")
emith(" %mfin = select i1 %mplt10, i32 %mplus3, i32 %mminus9\n")
emith(" %mmle2 = icmp sle i32 %mfin, 2\n")
emith(" %yinc = select i1 %mmle2, i32 1, i32 0\n")
emith(" %yfin = add i32 %yy, %yinc\n")
emith(" store i32 %yfin, ptr %yp\n")
emith(" store i32 %mfin, ptr %mp\n")
emith(" store i32 %dd, ptr %dp\n")
emith(" ret void\n")
emith("}\n")
}

View file

@ -104,6 +104,7 @@ function emit_program() -> void {
if g_uses_textrt { emit_text_prelude() } # @fn_str_upper/lower/trim/repeat/pad builders if g_uses_textrt { emit_text_prelude() } # @fn_str_upper/lower/trim/repeat/pad builders
if g_uses_textrt2 { emit_text2_prelude() } # @fn_str_replace/join/split builders if g_uses_textrt2 { emit_text2_prelude() } # @fn_str_replace/join/split builders
if g_uses_hashrt { emit_hash_prelude() } # @fn_hash_fnv1a / @fn_hash_crc32 byte hashers if g_uses_hashrt { emit_hash_prelude() } # @fn_hash_fnv1a / @fn_hash_crc32 byte hashers
if g_uses_datert { emit_datetime_prelude() } # @fn_days_from_civil / @fn_civil_from_days conversions
} }
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell # Flush the emitted IR. With a null path it goes to stdout (the pipe the shell

View file

@ -235,6 +235,18 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if is_vector_ns(meth) { return emit_vector_ns(meth, e) } if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
perr(`unknown builtin Vector.{meth}`) perr(`unknown builtin Vector.{meth}`)
} }
if (ns == "Duration") {
if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
perr(`unknown builtin Duration.{meth}`)
}
if (ns == "Date") {
if is_date_ns(meth) { return emit_date_ns(meth, e) }
perr(`unknown builtin Date.{meth}`)
}
if (ns == "DateTime") {
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
perr(`unknown builtin DateTime.{meth}`)
}
var bare: pointer = null var bare: pointer = null
let labels = new []pointer let labels = new []pointer
if (ns == "Screen") { if (ns == "Screen") {

View file

@ -5,6 +5,7 @@
function is_time_ns(meth: pointer) -> bool { function is_time_ns(meth: pointer) -> bool {
if (meth == "frame") or (meth == "delta") or (meth == "elapsed") or (meth == "now") { return true } if (meth == "frame") or (meth == "delta") or (meth == "elapsed") or (meth == "now") { return true }
if (meth == "since") { return true }
return false return false
} }
@ -19,6 +20,12 @@ function emit_time_ns(meth: pointer, e: Node) -> Val {
let f = emit_bind("load i32, ptr @L_frame") let f = emit_bind("load i32, ptr @L_frame")
return val(emit_bind(`mul i32 {f}, 1092`), "fixed") return val(emit_bind(`mul i32 {f}, 1092`), "fixed")
} }
if (meth == "since") { # seconds elapsed from a past instant to now
let past = emit_expr(e.kids[0]) # NON-DETERMINISTIC (reads the wall clock)
let nw = emit_bind("call i64 @time(ptr null)")
let n32 = emit_bind(`trunc i64 {nw} to i32`)
return val(emit_bind(`sub i32 {n32}, {past.code}`), "int")
}
# now: wall-clock seconds since the epoch — NON-DETERMINISTIC, for seeding / # now: wall-clock seconds since the epoch — NON-DETERMINISTIC, for seeding /
# telemetry only, never the lockstep simulation. # telemetry only, never the lockstep simulation.
let t = emit_bind("call i64 @time(ptr null)") let t = emit_bind("call i64 @time(ptr null)")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
program T {
entry {
# Duration — spans in whole seconds
print(Duration.hours(3)) # 10800
print(Duration.minutes(5) + Duration.seconds(30)) # 330
print(Duration.days(2)) # 172800
print(Duration.as_hours(10800)) # 3
print(Duration.as_days(172800)) # 2
# Date — civil day as days-since-1970, round-trips both ways
print(Date.new(1970, 1, 1)) # 0
print(Date.new(2000, 1, 1)) # 10957
let d = Date.new(2026, 8, 30)
print(Date.year(d)) # 2026
print(Date.month(d)) # 8
print(Date.day(d)) # 30
print(Date.weekday(Date.new(1970, 1, 1))) # 4 (Thursday)
print(Date.weekday(Date.new(2000, 1, 1))) # 6 (Saturday)
print(Date.is_leap(2000)) # 1
print(Date.is_leap(1900)) # 0
print(Date.is_leap(2024)) # 1
print(Date.days_in_month(2024, 2)) # 29 (leap Feb)
print(Date.days_in_month(2023, 2)) # 28
print(Date.days_in_month(2026, 12)) # 31 (December rollover)
let feb = Date.add_days(Date.new(2026, 1, 1), 31)
print(Date.month(feb)) # 2
print(Date.day(feb)) # 1
print(Date.diff_days(Date.new(2026, 1, 1), Date.new(2025, 1, 1))) # 365
# DateTime — instant as seconds-since-1970
let t = DateTime.from(2000, 1, 1, 12, 30, 15)
print(t) # 946729815
print(DateTime.year(t)) # 2000
print(DateTime.month(t)) # 1
print(DateTime.day(t)) # 1
print(DateTime.hour(t)) # 12
print(DateTime.minute(t)) # 30
print(DateTime.second(t)) # 15
print(DateTime.weekday(t)) # 6
print(DateTime.date(t)) # 10957
print(DateTime.hour(DateTime.add(0, Duration.hours(1)))) # 1
}
}

View file

@ -315,7 +315,8 @@
"time-frame", "time-frame",
"time-delta", "time-delta",
"time-elapsed", "time-elapsed",
"time-now" "time-now",
"time-since"
], ],
"types": [ "types": [
"type-bool", "type-bool",
@ -366,5 +367,39 @@
"vector-rotate", "vector-rotate",
"vector-angle", "vector-angle",
"vector-from_angle" "vector-from_angle"
],
"duration": [
"duration-seconds",
"duration-minutes",
"duration-hours",
"duration-days",
"duration-as_seconds",
"duration-as_minutes",
"duration-as_hours",
"duration-as_days"
],
"date": [
"date-new",
"date-year",
"date-month",
"date-day",
"date-weekday",
"date-is_leap",
"date-days_in_month",
"date-to_epoch",
"date-add_days",
"date-diff_days"
],
"datetime": [
"datetime-from",
"datetime-date",
"datetime-add",
"datetime-year",
"datetime-month",
"datetime-day",
"datetime-weekday",
"datetime-hour",
"datetime-minute",
"datetime-second"
] ]
} }

View file

@ -1401,6 +1401,41 @@ program LudicLsp {
if (meth == "delta") { return "Time.delta() -> fixed" } if (meth == "delta") { return "Time.delta() -> fixed" }
if (meth == "elapsed") { return "Time.elapsed() -> fixed" } if (meth == "elapsed") { return "Time.elapsed() -> fixed" }
if (meth == "now") { return "Time.now() -> int" } if (meth == "now") { return "Time.now() -> int" }
if (meth == "since") { return "Time.since(past) -> int" }
}
if (ns == "Duration") {
if (meth == "seconds") { return "Duration.seconds(n) -> int" }
if (meth == "minutes") { return "Duration.minutes(n) -> int" }
if (meth == "hours") { return "Duration.hours(n) -> int" }
if (meth == "days") { return "Duration.days(n) -> int" }
if (meth == "as_seconds") { return "Duration.as_seconds(span) -> int" }
if (meth == "as_minutes") { return "Duration.as_minutes(span) -> int" }
if (meth == "as_hours") { return "Duration.as_hours(span) -> int" }
if (meth == "as_days") { return "Duration.as_days(span) -> int" }
}
if (ns == "Date") {
if (meth == "new") { return "Date.new(year, month, day) -> int" }
if (meth == "year") { return "Date.year(date) -> int" }
if (meth == "month") { return "Date.month(date) -> int" }
if (meth == "day") { return "Date.day(date) -> int" }
if (meth == "weekday") { return "Date.weekday(date) -> int" }
if (meth == "is_leap") { return "Date.is_leap(year) -> bool" }
if (meth == "days_in_month") { return "Date.days_in_month(year, month) -> int" }
if (meth == "to_epoch") { return "Date.to_epoch(date) -> int" }
if (meth == "add_days") { return "Date.add_days(date, n) -> int" }
if (meth == "diff_days") { return "Date.diff_days(a, b) -> int" }
}
if (ns == "DateTime") {
if (meth == "from") { return "DateTime.from(year, month, day, hour, minute, second) -> int" }
if (meth == "date") { return "DateTime.date(dt) -> int" }
if (meth == "add") { return "DateTime.add(dt, span) -> int" }
if (meth == "year") { return "DateTime.year(dt) -> int" }
if (meth == "month") { return "DateTime.month(dt) -> int" }
if (meth == "day") { return "DateTime.day(dt) -> int" }
if (meth == "weekday") { return "DateTime.weekday(dt) -> int" }
if (meth == "hour") { return "DateTime.hour(dt) -> int" }
if (meth == "minute") { return "DateTime.minute(dt) -> int" }
if (meth == "second") { return "DateTime.second(dt) -> int" }
} }
if (ns == "Memory") { if (ns == "Memory") {
if (meth == "bytes") { return "Memory.bytes(n) -> pointer" } if (meth == "bytes") { return "Memory.bytes(n) -> pointer" }

View file

@ -33,6 +33,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/emit_collide.ludic") push(f, "selfhost/emit_collide.ludic")
push(f, "selfhost/emit_mem.ludic") push(f, "selfhost/emit_mem.ludic")
push(f, "selfhost/emit_time.ludic") push(f, "selfhost/emit_time.ludic")
push(f, "selfhost/emit_datetime.ludic")
push(f, "selfhost/emit_colorfn.ludic") push(f, "selfhost/emit_colorfn.ludic")
push(f, "selfhost/emit_color.ludic") push(f, "selfhost/emit_color.ludic")
push(f, "selfhost/emit_new.ludic") push(f, "selfhost/emit_new.ludic")

View file

@ -47,6 +47,7 @@ function cmd_selfhost_test() -> int {
sh_case("math3", "45 90 180 -135 30 60 90") sh_case("math3", "45 90 180 -135 30 60 90")
sh_case("transcend", "2718 1 148 1000 0 2303 1024 1414 3 250 0 1000 1016") sh_case("transcend", "2718 1 148 1000 0 2303 1024 1414 3 250 0 1000 1016")
sh_case("vector", "3 4 5 4 6 2 2 11 2 6 999 999 999 1570 5 10") sh_case("vector", "3 4 5 4 6 2 2 11 2 6 999 999 999 1570 5 10")
sh_case("datetime", "10800 330 172800 3 2 0 10957 2026 8 30 4 6 1 0 1 29 28 31 2 1 365 946729815 2000 1 1 12 30 15 6 10957 1")
sh_case("textsplit", "1 1 1 3 1 1 1 1 1 1 1") sh_case("textsplit", "1 1 1 3 1 1 1 1 1 1 1")
sh_case("hash", "-2128831035 -468965076 114400290 114400290 0 -873187034 1095738169 0 1364076727 -2114883783 -845898438 -845898438 -78065325 -78057399 -3750763034362895579 -5808556873153909620 -4100651535478758590 -4100651535478758590 0 -5451962507482445012 7256831767414464289") sh_case("hash", "-2128831035 -468965076 114400290 114400290 0 -873187034 1095738169 0 1364076727 -2114883783 -845898438 -845898438 -78065325 -78057399 -3750763034362895579 -5808556873153909620 -4100651535478758590 -4100651535478758590 0 -5451962507482445012 7256831767414464289")
sh_case("long", "1000000000000 1000000000001 1000000000005 3000000000000 1 1 1 -1000000000000 1000000 13") sh_case("long", "1000000000000 1000000000001 1000000000005 3000000000000 1 1 1 -1000000000000 1000000 13")