Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
c0777f4
feat: add sleep primitive and update main to test it
LuernOutOfOrder Feb 5, 2026
5e7a747
refactor: move sleep from primitive to task
LuernOutOfOrder Feb 5, 2026
cdeb511
feat: update the task structure to add a TaskBlockControl field, upda…
LuernOutOfOrder Feb 5, 2026
532b9cc
feat: add a new task primitive task_block_until and move some logic f…
LuernOutOfOrder Feb 5, 2026
e57c6c1
fix: trap_entry override hart id with trap frame
LuernOutOfOrder Feb 5, 2026
1082b92
feat: add a new task primitive task_awake_blocked and use it in timer…
LuernOutOfOrder Feb 5, 2026
28c894d
chore: format files
LuernOutOfOrder Feb 5, 2026
79da14f
feat: add the delay task primitive, improve the task_awake primitive …
LuernOutOfOrder Feb 6, 2026
33a83ea
chore: remove unused import
LuernOutOfOrder Feb 6, 2026
bddcad7
refactor: move the task sleep primitive function from sleep module to…
LuernOutOfOrder Feb 6, 2026
5e84006
docs(kernel): add a basic primitives documentation
LuernOutOfOrder Feb 6, 2026
0dc1c11
docs(kernel): add table of content to the primitives docs
LuernOutOfOrder Feb 6, 2026
cdb84ae
feat(tests): add beginning of task primitives test
LuernOutOfOrder Feb 6, 2026
5f2b81b
feat(linkers): update test mode linker ROM length to compile
LuernOutOfOrder Feb 9, 2026
ca2ebdb
feat(tests): add sleep test
LuernOutOfOrder Feb 9, 2026
1e45b48
chore: format file
LuernOutOfOrder Feb 9, 2026
7699c43
feat: remove init_trap_frame from a test, and comment the yield test …
LuernOutOfOrder Feb 9, 2026
48f3382
feat(tests): improve sleep test
LuernOutOfOrder Feb 9, 2026
f841d2a
feat(docs): update the primitives documentation and add data_structur…
LuernOutOfOrder Feb 9, 2026
50ac62e
feat: add file info header to primitives task file and update task sl…
LuernOutOfOrder Feb 9, 2026
3cb7357
refactor: remove debug implementation to RingBuffer type
LuernOutOfOrder Feb 9, 2026
cd23a58
chore: bump kernel version to 0.4.3
LuernOutOfOrder Feb 9, 2026
7961edb
chore: format and remove warning
LuernOutOfOrder Feb 9, 2026
231299a
refactor: remove no_mangle use on test_task_primitives_delay
LuernOutOfOrder Feb 9, 2026
9b79e7c
Merge pull request #115 from LuernOutOfOrder/108-add-sleepdelay
LuernOutOfOrder Feb 9, 2026
c6facc1
feat: update the RUN_QUEUE init, add the idle task, and update main t…
LuernOutOfOrder Feb 5, 2026
7813605
feat: update the task_context test to use new RUN_QUEUE and enable th…
LuernOutOfOrder Feb 5, 2026
3a9df13
refactor: update codebase to use RUN_QUEUE instead of BUFFER
LuernOutOfOrder Feb 9, 2026
dad1a47
chore: remove unused import
LuernOutOfOrder Feb 9, 2026
d22794b
feat: add a new CpusState structure and public API for it
LuernOutOfOrder Feb 9, 2026
9231cf5
feat: update trap entry to handle reschedule, update method and publi…
LuernOutOfOrder Feb 9, 2026
027e5cd
feat: update the dispatch clear rechedule bit log to be a constant de…
LuernOutOfOrder Feb 9, 2026
935b880
feat: update the task context to save and restore mstatus
LuernOutOfOrder Feb 10, 2026
6964d43
feat: update default mstatus in task context to 8
LuernOutOfOrder Feb 10, 2026
2c958f3
chore: remove unused import and format files
LuernOutOfOrder Feb 10, 2026
1d198d9
feat: update task context test to use updated offset and fix wrong im…
LuernOutOfOrder Feb 10, 2026
3d58327
feat: remove the test for the idle task in main, put the cfg with idl…
LuernOutOfOrder Feb 10, 2026
a27dcc7
chore: bump kernel version to 0.4.4
LuernOutOfOrder Feb 10, 2026
56b4299
docs(kernel): update the task documentation and add idle task section
LuernOutOfOrder Feb 10, 2026
ab62c06
chore: format files and remove warnings
LuernOutOfOrder Feb 10, 2026
309492b
feat: skipped the task context test and add the info header on misc file
LuernOutOfOrder Feb 10, 2026
3a37968
Merge pull request #116 from LuernOutOfOrder/107-add-idle-task
LuernOutOfOrder Feb 10, 2026
a7ec0ee
feat: add the run queue bitmap and update the run queue data structur…
LuernOutOfOrder Feb 10, 2026
41b97ce
feat: improve run and block queue type to handle multi core arch for …
LuernOutOfOrder Feb 10, 2026
3571e5e
feat(primitives): add basic bitmap datastructure
LuernOutOfOrder Feb 10, 2026
2330c85
feat(scheduler & task): update run_queue bitmap to use new Bitmap typ…
LuernOutOfOrder Feb 10, 2026
4abb5c4
feat(primitives & scheduler): add is_bitmap_zero to bitmap data struc…
LuernOutOfOrder Feb 12, 2026
f3c7273
feat(primitives & scheduler): update the bitmap and ring buffer primi…
LuernOutOfOrder Feb 12, 2026
4da9773
refactor: rename the old dispatch function to scheduler and update co…
LuernOutOfOrder Feb 12, 2026
b12dd8f
feat(primitives): working on the push to delta list, hard to do, wip
LuernOutOfOrder Feb 12, 2026
f7f2bc1
feat(primitives & tests): start adding DeltaList test, easier to test…
LuernOutOfOrder Feb 12, 2026
2c39d50
refactor: update the delta list to index_linked_list, delta was too m…
LuernOutOfOrder Feb 13, 2026
b3aa4f7
feat(primitives & tests): make the indexed linked list work
LuernOutOfOrder Feb 13, 2026
d219321
feat(primitives & tests): add more test for the indexed linked list push
LuernOutOfOrder Feb 13, 2026
8a4a38c
feat(primitives & tests): add pop and get_head_node methods with test…
LuernOutOfOrder Feb 13, 2026
613c47e
docs(kernel): update the data structure doc to include the new Indexe…
LuernOutOfOrder Feb 13, 2026
b5165f6
feat(primitives & tests): add a duplication check
LuernOutOfOrder Feb 13, 2026
fb8bf47
docs: update the docs for IndexedLinkedList in data structure
LuernOutOfOrder Feb 13, 2026
1a7cd0d
feat(scheduler & task primitives): update the scheduler to use new bl…
LuernOutOfOrder Feb 16, 2026
4169852
docs(scheduler): improve code comment on the need_resched flags in th…
LuernOutOfOrder Feb 16, 2026
e95789e
feat(scheduler & task primitives): update the sleep routine to correc…
LuernOutOfOrder Feb 16, 2026
90c0bb6
fix: the scheduler blocked queue not working with task primitives
LuernOutOfOrder Feb 16, 2026
077e77a
feat(primitives): update the indexed linked list to correctly pop
LuernOutOfOrder Feb 16, 2026
7cfe8a1
fix(arch riscv32): no more crash when trigger a reschedule, the runti…
LuernOutOfOrder Feb 17, 2026
42f2f80
feat(arch riscv32): remove the save of mstatus in save_context, and u…
LuernOutOfOrder Feb 17, 2026
f3c7490
fix(arch riscv32): trap entry to use mepc to return to task execution…
LuernOutOfOrder Feb 17, 2026
db707bc
feat(arch riscv32): improve mstatus handling on save and restore context
LuernOutOfOrder Feb 17, 2026
7825b87
feat: preemptif scheduler work
LuernOutOfOrder Feb 17, 2026
7bc3147
feat(arch riscv32): remove the hardcoded mstatus value, prefer handli…
LuernOutOfOrder Feb 17, 2026
8b15c37
docs(kernel): add basic scheduler doc
LuernOutOfOrder Feb 17, 2026
ea5eafb
docs(kernel): update the scheduler doc, add invariants and toc
LuernOutOfOrder Feb 19, 2026
537a3f5
docs(kernel): add the scheduling model section
LuernOutOfOrder Feb 19, 2026
10d2bc1
docs(kernel): update the scheduler invariants with temp invariant
LuernOutOfOrder Feb 19, 2026
afbaeb9
feat(tests): add the indexed linked list test suite
LuernOutOfOrder Feb 19, 2026
05db047
feat(tests): update riscv32 task_context test to use new scheduler qu…
LuernOutOfOrder Feb 19, 2026
a6ed074
feat(tests): update task_context and task primitives tests
LuernOutOfOrder Feb 19, 2026
c5a3934
feat(scheduler & tests): update the scheduler info header and add emp…
LuernOutOfOrder Feb 19, 2026
410bfed
chore: remove all warnings
LuernOutOfOrder Feb 19, 2026
3bf1f34
chore: remove unused import and clean unused variable
LuernOutOfOrder Feb 19, 2026
196cd04
feat(tests primitives): add missing IndexedLinkedList pop test
LuernOutOfOrder Feb 19, 2026
4e59f26
chore: format file and added scheduler test suite empty fn
LuernOutOfOrder Feb 19, 2026
e624955
chore: refactor codebase for make check, improve codebase, format fil…
LuernOutOfOrder Feb 19, 2026
78a7969
feat(tests): add the scheduler_test_suite to test_suites fn
LuernOutOfOrder Feb 19, 2026
31ec292
feat(linkers): update test_mode linker, smelly I know
LuernOutOfOrder Feb 19, 2026
2ff0dcb
chore: bump kernel version to 0.4.5
LuernOutOfOrder Feb 19, 2026
5ca34ec
chore: remove Debug impl in task_context structure and indexed_linked…
LuernOutOfOrder Feb 19, 2026
88338a0
Merge pull request #117 from LuernOutOfOrder/106-preemptif-scheduler-…
LuernOutOfOrder Feb 19, 2026
b6acb6b
Merge branch 'main' into develop
LuernOutOfOrder Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
[package]
name = "lrnrtos"
version = "0.4.2"
version = "0.4.5"
edition = "2024"

[[bin]]
name= "lrnrtos"
name = "lrnrtos"
test = false

[dependencies]
arrayvec = {version = "0.7.6", default-features = false}
arrayvec = { version = "0.7.6", default-features = false }

[features]
default = ["logs", "kprint"]
# Enable kernel logs
logs = []
# Enable early print using statically define uart device
kprint = []
# Switch to test mode
test = []
# Enable the idle task.
idle_task = []
57 changes: 57 additions & 0 deletions Documentation/kernel/data_structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Kernel data structure

<!--toc:start-->
- [Kernel data structure](#kernel-data-structure)
- [Description](#description)
- [RingBuffer](#ringbuffer)
- [Invariants](#invariants)
- [AlignedStack16](#alignedstack16)
- [Invariants](#invariants-1)
- [IndexedLinkedList](#indexedlinkedlist)
- [Invariants](#invariants-2)
<!--toc:end-->

## Description

The kernel as multiple data structure implementation inside the codebase, they are useful to store and manipulate data inside the kernel.
These are all the data structure implemented inside the kernel.

### RingBuffer

A simple Ring buffer, used as an FIFO. If the RingBuffer is full and you try to push anyways, it will be abort.
It's like a close RingBuffer, you can't push if it's full and you don't pop before.

#### Invariants

- Length is always in [0, capacity].
- The length is len - 1; there's always an empty slot in the array.
- Head and tail always remain within the backing array bounds.
- Push is only valid when the buffer is not full; violating this is a logic error (abort).
- Pop is only valid when the buffer is not empty; violating this is a logic error (abort).

### AlignedStack16

This is just a structure wrapping a buffer of bytes, but the structure use the `#[repr(align(16))]`.
This type is used when you need a stack on a buffer, and the `sp` must be aligned on `16 bytes`.

#### Invariants

- The backing storage is always 16-byte aligned.
- Any stack pointer derived from this type must remain 16-byte aligned at all call boundaries.
- This type provides a memory-layout guarantee only; it does not validate stack usage correctness.

### IndexedLinkedList

A linked list but store in an array, so each node is accessed from it's index in the array.
This is better to use this data structure as a side storage, like we use it to store blocked task.
Task are already store in a TaskList, so in the IndexedLinkedList used for blocked task we only store task id, and task awake tick for now.

#### Invariants

- All node should be accessible from the head node.
- The list is sorted naturally from the `value` field of a node.
- The `count` field should reflect the number of accessible node in the list.
- The list is empty when `count`, `head` and `tail` are equal to 0.
- If the `next_node` of a node is some, this `next_node` is valid.
- If the `next_node` of a node is none, then this node is the `tail`.
- The node `id` is unique, you can't add the same `id` in the list.
78 changes: 78 additions & 0 deletions Documentation/kernel/primitives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Kernel primitives types and functions

<!--toc:start-->
- [Kernel primitives types and functions](#kernel-primitives-types-and-functions)
- [Description](#description)
- [Primitive type](#primitive-type)
- [Description](#description-1)
- [Task primitive](#task-primitive)
- [Description](#description-2)
- [yield](#yield)
- [sleep](#sleep)
- [task_awake_blocked](#taskawakeblocked)
- [Invariants](#invariants)
<!--toc:end-->

## Description

This document describes the kernel primitives.

In the context of this kernel, a *primitive* is defined as a low-level construct that
**directly affects kernel state or execution context**.
If using a type or function can change global kernel behavior, scheduling state,
or execution flow, it is considered a primitive.

Two categories of primitives are documented here:

- **Primitive types**: low-level types whose correct usage is required to preserve
kernel invariants. These types are not mere data structures; they encode execution,
synchronization, or memory-layout guarantees that the kernel relies on.
Examples include synchronization objects such as mutexes or types enforcing
strict alignment or execution constraints.

- **Task primitives**: execution control operations that may only be used from
task context. These primitives modify the scheduling or blocking state of the
current task and therefore have observable effects on global kernel execution.

Pure data structures that do not alter kernel state or execution context are
documented separately and are not considered primitives, even if they are used
internally by primitive implementations.
You can find data structure type here: `Documentation/kernel/data_structure.md`.

### Primitive type

#### Description

There are currently no primitive type implemented in the kernel.

### Task primitive

#### Description

To handle task correctly, the kernel need some primitives but only used in a task context.
These type can't and must not be used anywhere else than inside a task.

#### yield

Used in cooperative scheduling, when a task use `yield`, it will save it's context, and call a re-schedule.
It is used when you want a task to let another task take the control.

#### sleep

Put the task using the `sleep` primitive function to sleep for the given time.
It blocked the task until the kernel `GLOBAL_TICK` is equal or superior to the current tick + the given tick.
You can consider the given tick as `1ms`.

#### task_awake_blocked

Awake the oldest blocked task if it can.
This primitive is called from a timer interrupt, and only from a timer interrupt.
The timer interrupt will give the primitive `task_awake_blocked` the current `GLOBAL_TICK`, after updating it from the current interrupt.
The primitive will get the `oldest blocked task`, from the `BLOCKED_QUEUE`, then it'll check the reason why this task is blocked, and awake it if possible.

#### Invariants

- Task primitives must only be called from task context.
- The scheduler must be initialized before any task primitive is used.
- Time-based primitives rely on a functional timer subsystem.
- Principal data structure such as `RUN_QUEUE` and `BLOCKED_QUEUE` must be initialized before any task primitive is used.
57 changes: 57 additions & 0 deletions Documentation/kernel/scheduler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Kernel scheduler

<!--toc:start-->
- [Kernel scheduler](#kernel-scheduler)
- [Description](#description)
- [Queues](#queues)
- [Run queue](#run-queue)
- [Blocked queue](#blocked-queue)
- [Preemption](#preemption)
- [Scheduling model](#scheduling-model)
- [Invariants](#invariants)
<!--toc:end-->

## Description

The kernel scheduler is in charge of managing which task to run, updating the run queue and the blocked queue and updating the task state.

## Queues

There's 2 queues used in the scheduler. The `run queue` and the `blocked queue`. Each queue is specific for a `CPU core`. The `CPU core 1` has a different `run queue` than the `CPU core 2`.

### Run queue

The `run queue` contains all the task ready to be run. In the `ready` task state.

The `run queue` work using a `FIFO` queue, there's a queue for each `priority`, up to `32 queues`.
To find which queue to use to execute a task, instead of iterating over all queues, we use a `bitmap`.
There's only `32 priorities`, so we use a `u32 bitmap`, each bit representing a `run queue`, if the bit is set, there's at least 1 task to run.
Else, the queue is empty.

### Blocked queue

The `blocked queue` contains all the task currently blocked. With different block reasons.
But currently the `blocked queue` contains uniquely the task blocked using the `sleep` task primitive.

The `blocked queue` work using an `indexed linked list`, the list is sorted from the shortest awake tick to the largest awake tick.
The list is manage using `head` and `tail`, a bit like a `ring buffer` data structure.
So when we need to check the next task to awake, we just check the `head` of the `blocked queue`.
If it can be awake, it will update the `need_resched` flag, then it'll trigger a reschedule, the scheduler will be able to awake the task, and move it from the `blocked queue` to the `run queue`.

## Preemption

The scheduler is preemptive, meaning that if a task as a higher priority than the current task, it will run this higher priority task.
The current task having a lowest priority, will be saved, and re-execute when there's no higher priority task to run.

## Scheduling model

The current scheduling model is a `cooperative priority based`. Meaning that if there's `2 task` with the same `priority`, if they don't call `yield`, one task will `always run`.
Making the other task `starving`.
So if you want to switch from a `task` to another one, you need to use `cooperative` functions, like `yield` or `sleep`.

## Invariants

- The scheduler need at least one task in the `run queue`, if the `run queue` is empty, it will try to run the idle task. Make sure that there's always at least one task in the `run queue`, or enable the `idle task` feature.
- The scheduler assume that the `CpusState` is initialized to access the `CPU core scheduler state`.
- The scheduler can be called from the `trap epilogue`, if so, a `trap frame`, should be available and accessible for the scheduler to run on.
- For now the scheduler assume that there's always a `current running task`, if not, and the scheduler is triggered, this could lead to UB.
18 changes: 18 additions & 0 deletions Documentation/kernel/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Purpose](#purpose)
- [Structure](#structure)
- [How task is store](#how-task-is-store)
- [Idle task](#idle-task)
- [Invariants](#invariants)
- [References](#references)
<!--toc:end-->
Expand All @@ -31,11 +32,20 @@ enum TaskState {
Terminated,
}

pub enum TaskBlockControl {
// Store the awake tick for task awakening.
AwakeTick(usize),
// No reason for the task block
None,
}

#[repr(C)]
struct Task {
// Arch dependant context, don't handle this field in task, only use struct method when
// interacting with it.
context: TaskContext,
// Task block control, define the reason the task is blocked.
block_control: TaskBlockControl,
// Fn ptr to task entry point, this must never return.
// This will surely disappear
func: fn() -> !,
Expand All @@ -53,6 +63,8 @@ pub struct TaskContext {
pub address_space: [u32; 2],
pub pc: u32,
pub sp: u32,
pub ra: u32,
pub mstatus: u32,
pub flags: [u8; 3],
pub instruction_register: u8,
}
Expand All @@ -76,6 +88,12 @@ pub struct TaskList {
}
```

## Idle task

The idle task is used to ensure that the kernel as always at least one task able to run.
This task is created at the lowest priority to ensure it does not use any CPU time if there are higher priority application tasks in the run queue.
It is not possible to update the idle task, it's a static defined task.

## Invariants

- The task's function must never return.
Expand Down
22 changes: 22 additions & 0 deletions Documentation/kernel/timing_helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Kernel timing helpers

<!--toc:start-->
- [Kernel timing helpers](#kernel-timing-helpers)
- [Description](#description)
- [delay](#delay)
- [Invariants](#invariants)
<!--toc:end-->

## Description

The kernel sometimes need to use some helpers to handle or manage timing. Here's a list of some helpers to help with that.

### delay

Block the CPU for the given time, in `ms`.
This is not really recommended to use, it will not put the CPU to sleep, just waiting for the next timer interrupt.
If you need a task to wait or something else, prefer the use of `yield`.

### Invariants

- The scheduler must be initialized before any timing helpers is used.
4 changes: 2 additions & 2 deletions linkers/linker_test_mode.ld
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
ENTRY(kstart)

MEMORY {
RAM (rwx) : ORIGIN = 0x80200000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x80200000, LENGTH = 256K
/* Not real ROM or flash from qemu virt machine, just use another RAM reg for now */
ROM (rx) : ORIGIN = 0x80000000, LENGTH = 200K
ROM (rx) : ORIGIN = 0x80000000, LENGTH = 256K
}

SECTIONS {
Expand Down
2 changes: 2 additions & 0 deletions src/arch/riscv32/asm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ global_asm! {
include_str!("set_kernel_sp.S"),
// Yield function for context switch and scheduling
include_str!("yield.S"),
// Sleep function for task and scheduling
include_str!("sleep.S"),
// Scheduler context switch
include_str!("sched_context.S"),
// All task context offsets
Expand Down
27 changes: 27 additions & 0 deletions src/arch/riscv32/asm/restore_context.S
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,31 @@ restore_context:
load_gp_context %i
.set i, i+1
.endr
# Update mstatus
li t0, ((3 << 11) | (1 << 3))
csrrs x0, mstatus, t0
ret

.global trap_restore_context
.type trap_restore_context, @function
trap_restore_context:
# Move current task context struct save in caller in a0 reg to t6
mv t6, a0
# Update sp
# Restore sp from current task context structure
lw t0, OFFSET_SP(t6)
mv sp, t0
# Update mepc
lw t0, OFFSET_PC(t6)
csrw mepc, t0
# Restore current task context using GNU macro
# GNU macros from `src/arch/riscv32/asm/gnu_macro.S`
.set i, 1
.rept 31
load_gp_context %i
.set i, i+1
.endr
# Update mstatus
li t0, ((3 << 11) | (1 << 7))
csrrs x0, mstatus, t0
mret
25 changes: 25 additions & 0 deletions src/arch/riscv32/asm/save_context.S
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,37 @@
.global save_context
.type save_context, @function
save_context:
# Clear mstatus.MPP and mstatus.mie to avoid interruption while saving and switching context
li t0, ((3 << 11) | (1 << 3))
csrrc x0, mstatus, t0
# Move current task context struct save in caller in a0 reg to t6
mv t6, a0
# Store ra in task context structure
sw a1, OFFSET_RA(t6)
# Store word from t0(sp) in context structure
sw a2, OFFSET_SP(t6)
sw a3, OFFSET_PC(t6)
# Save current task context using GNU macro
# GNU macros from `src/arch/riscv32/asm/gnu_macro.S`
.set i, 1
.rept 31
save_gp_context %i
.set i, i+1
.endr
ret

.global trap_save_context
.type trap_save_context, @function
trap_save_context:
# Clear mstatus.MPP and mstatus.mie to avoid interruption while saving and switching context
li t0, ((3 << 11) | (1 << 3))
csrrc x0, mstatus, t0
# Move current task context struct save in caller in a0 reg to t6
mv t6, a0
# Store ra in task context structure
sw a1, OFFSET_PC(t6)
# Store word from t0(sp) in context structure
sw a2, OFFSET_SP(t6)
# Save current task context using GNU macro
# GNU macros from `src/arch/riscv32/asm/gnu_macro.S`
.set i, 1
Expand Down
Loading