authorchloe caruso <git@paperclover.net> 2024-06-01 21:21:51-07:00
committerchloe caruso <git@paperclover.net> 2024-06-01 21:21:51-07:00
logdce2f82e1b6a711155e4a9a097e83cd0717b33a6
treecd200116572339279769b3171b5a258cd8ae3c12
parentc92fd86aaae8e8f251562fa54b8ec350542ac294

implement binary operand stuff


17 files changed, 927 insertions(+), 705 deletions(-)

CHANGELOG.md+3
......@@ -2,12 +2,15 @@
22
33- feat(sema): coersion: numeric\_literal -> float
44- feat(sema): prevent passing non-runtime compatible types like `type` to opaque functions
5- feat(sema): properly validate and coerce binary operations
6- feat(sema): string concat with `+`
57- feat(transform): lhs of assignments now validated
68- feat(transform): mark useless discard
79- feat(transform): parse string escapes properly
810- feat(link): print string escapes properly
911- feat: implement the `comptime` keyword (many comptime ops are not implemented)
1012- feat: add `@jsNew` to create JavaScript `new` expressions
13- fix(parse): top-level if statements previously required semicolons
1114- fix(sema): properly coerce assignments
1215- fix(transform): empty block generated in top level code tripped sema assertion
1316- fix(link): emit parenthesis to preserve operator precedence
build+5-5
......@@ -1,6 +1,6 @@
11#!/bin/sh
22# This script will build the compiler on any POSIX system.
3# It automatically installs a Zig compiler to ./zig-cache
3# It automatically installs a Zig compiler to ./.zig-cache
44# if needed. The output will be in ./zig-out/bin.
55#
66# `curl` and `tar` is required in order to download and
......@@ -11,7 +11,7 @@ cd -- "$(dirname -- "$0")"
1111ZIG_VERSION=$(grep .minimum_zig_version build.zig.zon \
1212 | cut -d '"' -f 2)
1313
14ZIG="zig-cache/zig/$ZIG_VERSION/zig"
14ZIG=".zig-cache/zig/$ZIG_VERSION/zig"
1515if [ ! -f "$ZIG" ]; then
1616 OS=$(uname -s | tr '[:upper:]' '[:lower:]')
1717 if [ "$OS" = "darwin" ]; then
......@@ -40,12 +40,12 @@ if [ ! -f "$ZIG" ]; then
4040 exit 1
4141 fi
4242
43 mkdir -p zig-cache/zig/$ZIG_VERSION
43 mkdir -p .zig-cache/zig/$ZIG_VERSION
4444 curl -fsSL --retry 3 --retry-delay 3 $URL \
45 | tar -xJ --strip-components=1 -C zig-cache/zig/$ZIG_VERSION
45 | tar -xJ --strip-components=1 -C .zig-cache/zig/$ZIG_VERSION
4646 if [ "$($ZIG version)" != "$ZIG_VERSION" ]; then
4747 echo "Zig failed to install."
48 rm -rf zig-cache/zig/$ZIG_VERSION
48 rm -rf .zig-cache/zig/$ZIG_VERSION
4949 exit 1
5050 fi
5151fi
build.cmd+9-9
......@@ -1,7 +1,7 @@
11@ECHO OFF
22@REM This script will build the compiler on any Windows system
3@REM It automatically installs a Zig compiler to ./zig-cache
4@REM The output will be in ./zig-out/bin
3@REM It automatically installs a Zig compiler to \/.zig-cache
4@REM The output will be in .\zig-out\bin
55
66SET ROOT=%~dp0
77FOR /F "tokens=3 delims=, " %%i IN (
......@@ -9,9 +9,9 @@ FOR /F "tokens=3 delims=, " %%i IN (
99 "%ROOT%build.zig.zon"'
1010) DO SET ZIG_VERSION=%%i
1111SET ZIG_VERSION=%ZIG_VERSION:~1,-1%
12IF NOT EXIST "%ROOT%zig-cache\zig\%ZIG_VERSION%\zig.exe" GOTO :find_powershell
12IF NOT EXIST "%ROOT%.zig-cache\zig\%ZIG_VERSION%\zig.exe" GOTO :find_powershell
1313:build
14"%ROOT%zig-cache\zig\%ZIG_VERSION%\zig.exe" build %*
14"%ROOT%.zig-cache\zig\%ZIG_VERSION%\zig.exe" build %*
1515EXIT /b %ERRORLEVEL%
1616:find_powershell
1717FOR %%i IN (pwsh powershell) DO WHERE %%i /Q && SET PS=%%i && GOTO :download
......@@ -20,10 +20,10 @@ EXIT /b 1
2020:download
2121SET ZIG_ARCH=x86_64
2222SET URI=https://ziglang.org/builds/zig-windows-%ZIG_ARCH%-%ZIG_VERSION%.zip
23SET X=$null = New-Item -ItemType Directory -Force -Path zig-cache\zig
24SET X=%X%;Invoke-RestMethod -Uri %URI% -OutFile zig-cache/zig-%ZIG_VERSION%.zip
25SET X=%X%;Expand-Archive zig-cache/zig-%ZIG_VERSION%.zip -DestinationPath zig-cache/zig
26SET X=%X%;Remove-Item zig-cache/zig-%ZIG_VERSION%.zip
23SET X=$null = New-Item -ItemType Directory -Force -Path .zig-cache\zig
24SET X=%X%;Invoke-RestMethod -Uri %URI% -OutFile .zig-cache/zig-%ZIG_VERSION%.zip
25SET X=%X%;Expand-Archive .zig-cache/zig-%ZIG_VERSION%.zip -DestinationPath .zig-cache/zig
26SET X=%X%;Remove-Item .zig-cache/zig-%ZIG_VERSION%.zip
2727%PS% -NoProfile -Command "%X%"
28MOVE zig-cache\zig\zig-windows-%ZIG_ARCH%-%ZIG_VERSION% zig-cache\zig\%ZIG_VERSION% >NUL
28MOVE .zig-cache\zig\zig-windows-%ZIG_ARCH%-%ZIG_VERSION% .zig-cache\zig\%ZIG_VERSION% >NUL
2929GOTO :build
build.zig+14-14
......@@ -59,22 +59,22 @@ pub fn build(b: *std.Build) void {
5959 }
6060
6161 // Language Reference Generator
62 const langref_generator = b.addExecutable(.{
63 .name = "generate-langref",
64 .root_source_file = b.path("src/langref/generate-langref.zig"),
62 const manual_generator = b.addExecutable(.{
63 .name = "generate-man",
64 .root_source_file = b.path("src/manual/generate-man.zig"),
6565 .target = native_target,
6666 .optimize = .Debug,
6767 .link_libc = true,
6868 });
69 langref_generator.root_module.addImport("build_options", native_debug.opts_mod);
70 langref_generator.root_module.addImport("core", native_debug.core);
71 const langref_run = b.addRunArtifact(langref_generator);
72 langref_run.addFileArg(native_debug.compiler.getEmittedBin());
73 langref_run.addFileArg(b.path("doc/language-reference.md"));
74 langref_run.addFileArg(b.path("src/langref/template.html"));
75 const langref_out = langref_run.addOutputFileArg("language-reference.html");
76 const install_langref = b.addInstallFile(langref_out, "doc/README.html");
77 b.step("langref", "build language reference").dependOn(&install_langref.step);
69 manual_generator.root_module.addImport("build_options", native_debug.opts_mod);
70 manual_generator.root_module.addImport("core", native_debug.core);
71 const manual_run = b.addRunArtifact(manual_generator);
72 manual_run.addFileArg(native_debug.compiler.getEmittedBin());
73 manual_run.addFileArg(b.path("doc/language-manual.md"));
74 manual_run.addFileArg(b.path("src/manual/template.html"));
75 const manual_out = manual_run.addOutputFileArg("language-reference.html");
76 const install_manual = b.addInstallFile(manual_out, "doc/README.html");
77 b.step("manual", "build language reference").dependOn(&install_manual.step);
7878
7979 // const wasm_api = b.addExecutable(.{
8080 // .name = "compiler",
......@@ -137,8 +137,8 @@ pub fn build(b: *std.Build) void {
137137 ).step);
138138 }
139139
140 package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "release/CHANGELOG.md").step);
141 package.dependOn(&b.addInstallFile(langref_out, "release/README.html").step);
140 package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step);
141 package.dependOn(&b.addInstallFile(manual_out, "package/README.html").step);
142142 }
143143}
144144
demo.todo created+10
......@@ -0,0 +1,10 @@
1fn something(a: int) -> bool {
2 if a == 0 {
3 return false;
4 }
5
6 a > 100
7}
8
9let print = @js("console").log;
10print(something(0xFF), something(0));
doc/language-manual.md created+160
......@@ -0,0 +1,160 @@
1# New Language Project
2
3This is a high-level, general purpose programming language.
4
5Key goals of this project are:
6
7- **Ease of Use**: Use of type inference keeps code concise and easy to read and write.
8- **Fast Iteration**: The compiler is designed for quick incremental compilation and hot module reloading. (note: incremental is not yet available)
9- TODO: some more
10
11It primarily focuses on merging some great points of Rust, Zig, and JavaScript's language designs:
12
13- **Rust** - Expressive control flow that is easy to write.
14- **Zig** - Execute any code at compile time, everything is an expression.
15- **JavaScript** - Garbage collected values with a versitile type system.
16
17## Standard Library
18
19Currently, there is no standard library. One will be introduced later.
20
21## Hello World
22
23```lang:run
24let print = @js("console").log;
25print("Hello, world!");
26```
27
28The `print` function is implemented by getting an external reference to the `console` JavaScript global, and then retriving the `log` property from it.
29
30## Primative Data Types
31
32| name | ts equivilent | description |
33| ---------- | --------------------- | ---------------------------------------------- |
34| `int` | `number` | a number in signed 32-bit range |
35| `float` | `number` | a 64-bit floating point number |
36| `bool` | `boolean` | either `true` or `false` |
37| `string` | `string` | a sequence of text characters |
38| `void` | `undefined` or `null` | detectable lack of a value |
39| `opaque` | `unknown` or `any` | could be any value |
40| `noreturn` | `never` | control flow prevents this value from existing |
41
42> **note**: `bigint` is planned but not added
43
44## Built-in Constants
45
46In addition to the above types being built-in constants, the following are available.
47
48| name | description |
49| ------- | ------------------------------------- |
50| `true` | boolean true (result of `1 == 1`) |
51| `false` | boolean false (result of `1 == 2`) |
52| `std` | handle to the standard library module |
53
54These constants are reserved names, and it is an error to re-declare them.
55
56```lang:build
57let true = 1 == 0;
58```
59
60## Variables
61
62There are two types of variables, `let` and `var`.
63
64`let` bindings cannot be overridden, and should be thought of as aliases of the expression within. Referring to these identifiers is equivilent to inlining it's where it was used, aside from side effect duplication.
65
66```lang:run
67let hello: int = 1;
68let world = hello + 2;
69let hello = hello * world;
70
71@js("console").log(hello);
72```
73
74`var` bindings create mutable memory locations at runtime. This means they can be re-assigned to:
75
76```lang:run
77var message = "first message";
78
79if @js("Math").random() > 0.5 {
80 message = "second message";
81}
82
83@js("console").log(message);
84```
85
86Using `let` is preferred whenever possible, as it will lead to more readable code, and the compiler is able to optimize it better. The above can be rewritten as:
87
88```lang:run
89let message = if @js("Math").random() > 0.5 {
90 "second message"
91} else {
92 "first message"
93};
94
95@js("console").log(message);
96```
97
98It should be noted that numeric literals are a comptime-only data type, meaning it cannot be assigned to a `var` without an explicit type annotation. Since `int` is a strict subset of valid `float`s, the coersion will implicitly be a float, but a compiler warning will be emitted.
99
100```lang
101var number = 0; // `number` is a `float`, but not explicit
102var number: int = 0; // `number` is `int`
103var number: float = 0; // `number` is `float`
104```
105
106## Functions
107
108Functions are declared with the `fn` keyword. Parameter types must be annotated. The function body is a block, which may use implicit return to return.
109
110```lang:run
111fn add(a: int, b: int) -> int {
112 a + b
113}
114
115let print = @js("console").log;
116print("Sum of 2 and 7:", add(2, 7));
117```
118
119> **Bug**: Return types are currently required. Omitting the `-> type` syntax currently makes the return type `void`, but this will be changed in the future to be an inferred return type.
120
121The `return` expression can be used within functions to return early.
122
123```lang:run
124fn something(a: int) -> bool {
125 if a == 0 {
126 return false;
127 }
128
129 a > 100
130}
131
132let print = @js("console").log;
133print(something(0xFF), something(0));
134```
135
136## Number Literals
137
138TODO
139
140## String Literals
141
142TODO
143
144## JavaScript Interop
145
146A global variable handle can be retrived with `@js`. We've seen this in the hello world. The type of this value is a special type called `opaque`, which disables type-checking.
147
148> **Note**: A future update will improve on the type-safety of `opaque`
149
150Many JS-exclusive actions are exposed via built-in functions, such as using `@jsNew` for calling constructors.
151
152```lang:run
153let print = @js("console").log;
154
155fn currentYear() -> int {
156 @jsNew(@js("Date")).getFullYear()
157}
158
159print(currentYear());
160```
doc/language-reference.md deleted-157
......@@ -1,157 +0,0 @@
1# New Language Project
2
3This is a high-level, general purpose programming language.
4
5Key goals of this project are:
6
7- **Ease of Use**: Use of type inference keeps code concise and easy to read and write.
8- **Fast Iteration**: The compiler is designed for quick incremental compilation and hot module reloading.
9- TODO: some more
10
11It primarily focuses on merging some great points of Rust, Zig, and JavaScript's language designs:
12
13- **Rust** - Expressive control flow that is easy to write.
14- **Zig** - Execute any code at compile time, everything is an expression.
15- **JavaScript** - Garbage collected values with a versitile type system.
16
17## Standard Library
18
19Currently, there is no standard library. One will be introduced later.
20
21## Hello World
22
23```lang:run
24let print = @js("console").log;
25print("Hello, world!");
26```
27
28The `print` function is implemented by getting an external reference to the `console` JavaScript global, and then retriving the `log` property from it.
29
30## Primative Data Types
31
32| name | ts equivilent | description |
33| ---------- | --------------------- | ---------------------------------------------- |
34| `int` | `number` | a number in signed 32-bit range |
35| `float` | `number` | a 64-bit floating point number |
36| `bool` | `boolean` | either `true` or `false` |
37| `string` | `string` | a sequence of text characters |
38| `void` | `undefined` or `null` | detectable lack of a value |
39| `noreturn` | `never` | control flow prevents this value from existing |
40
41> **note**: `bigint` is planned but not added
42
43## Built-in Constants
44
45In addition to the above types being built-in constants, the following are available.
46
47| name | description |
48| ------- | ------------------------------------- |
49| `true` | boolean true (result of `1 == 1`) |
50| `false` | boolean false (result of `1 == 2`) |
51| `std` | handle to the standard library module |
52
53These constants are reserved names, and it is an error to re-declare them.
54
55```lang:build
56let true = 1 == 0;
57```
58
59## Variables
60
61There are two types of variables, `let` and `var`.
62
63`let` bindings cannot be overridden, and should be thought of as aliases of the expression within. Referring to these identifiers is equivilent to inlining it's where it was used, aside from side effect duplication.
64
65```lang:run
66let hello: int = 1;
67let world = hello + 2;
68let hello = hello * world;
69
70@js("console").log(hello);
71```
72
73`var` bindings create mutable memory locations at runtime. This means they can be re-assigned to:
74
75```lang:run
76var message = "first message";
77
78if @js("Math").random() > 0.5 {
79 message = "second message";
80}
81
82@js("console").log(message);
83```
84
85Using `let` is preferred whenever possible, as it will lead to more readable code, and the compiler is able to optimize it better. The above can be rewritten as:
86
87```lang:run
88let message = if @js("Math").random() > 0.5 {
89 "second message"
90} else {
91 "first message"
92};
93
94@js("console").log(message);
95```
96
97It should be noted that numeric literals are a comptime-only data type, meaning it cannot be assigned to a `var` without an explicit type annotation. Since `int` is a strict subset of valid `float`s, the coersion will implicitly be a float, but a compiler warning will be emitted.
98
99```lang
100var number = 0; // `number` is a `float`, but not explicit
101var number: int = 0; // `number` is `int`
102var number: float = 0; // `number` is `float`
103```
104
105## Functions
106
107Functions are declared with the `fn` keyword. Parameter types must be annotated. The function body is a block, which may use implicit return to return.
108
109```lang:run
110fn add(a: int, b: int) -> int {
111 a + b
112}
113
114let print = @js("console").log;
115print("Sum of 2 and 7" + add(2, 7));
116```
117
118> **Bug**: Return types are currently required. Omitting the `-> type` syntax currently makes the return type `void`, but this will be changed in the future to be an inferred return type.
119
120The `return` expression can be used within functions to return early.
121
122```lang:run
123fn something(a: int) -> bool {
124 if a == 0 {
125 return false;
126 }
127
128 a > 100
129}
130
131let print = @js("console").log;
132print(something(0xFF), something(0));
133```
134
135## Number Literals
136
137TODO
138
139## String Literals
140
141TODO
142
143## JavaScript Interop
144
145A global variable handle can be retrived with `@js`. We've seen this in the hello world. The plan is to eventually replace this construct with something else, but implementing a function like this is extremely trivial for the compiler.
146
147Many JS-exclusive actions are exposed via built-in functions, such as using `@jsNew` for calling constructors.
148
149```lang:run
150let print = @js("console").log;
151
152fn currentYear() -> int {
153 @jsNew(@js("Date")).getFullYear()
154}
155
156print(currentYear());
157```
src/core/InternPool.zig+8
......@@ -288,6 +288,14 @@ pub const Value = enum(u32) {
288288 pub fn toFloat(val: Value, ip: *const InternPool) ?f64 {
289289 return switch (val.valueTag(ip)) {
290290 inline .comptime_float, .float => |tag| val.valueData(ip, tag).get(ip).decode(),
291 .int32 => @floatFromInt(val.valueData(ip, .int32)),
292 else => null,
293 };
294 }
295
296 pub fn toInt32(val: Value, ip: *const InternPool) ?i32 {
297 return switch (val.valueTag(ip)) {
298 .int32 => val.valueData(ip, .int32),
291299 else => null,
292300 };
293301 }
src/core/JsLink.zig+1-1
......@@ -193,10 +193,10 @@ fn printBlockContents(
193193 for (list.slice(tree)) |stmt| {
194194 try link.printExpr(tree, stmt, writer);
195195 switch (tree.nodeTag(stmt)) {
196 else => try writer.writeAll(";"),
197196 .stmt_block,
198197 .stmt_if,
199198 => {},
199 else => try writer.writeAll(";"),
200200 }
201201 try writer.writeAll("\n");
202202 }
src/core/Parse.zig+30-7
......@@ -245,8 +245,20 @@ fn parseModuleMembers(p: *Parse) Error!SubList {
245245 return p.failUnexpectedToken(.statement);
246246 }
247247
248 const first_tag = p.currentTag();
249
248250 try p.scratch.append(p.gpa, try p.parseExpression(true));
249 _ = try p.expect(.semi);
251
252 switch (first_tag) {
253 // some control flow and blocks are allowed to not have a
254 // trailing semicolons. this is a stylistic choice.
255 .kw_if,
256 .start_brace,
257 => {},
258
259 // TODO: make semicolons optional, but emit a warning
260 else => _ = try p.expect(.semi),
261 }
250262 },
251263 };
252264
......@@ -423,9 +435,7 @@ fn parseBlockContents(p: *Parse) Error!struct { Node.Tag, SubList } {
423435 if (p.currentTag() == .end_brace) break .block_implicit_return;
424436 },
425437
426 // TODO: make kw_break and kw_return REQUIRE a semicolon, as
427 // they are noreturn statements, doing implicit break on
428 // them makes no sense
438 // TODO: make semicolons optional, but emit a warning
429439
430440 else => {
431441 if (p.eatToken(.semi) == null) {
......@@ -456,13 +466,13 @@ fn parseVarDecl(p: *Parse) Error!NodeIndex {
456466 _ = try p.expect(.id);
457467
458468 const data_type: OptionalNodeIndex = if (p.eatToken(.colon)) |_|
459 optionalNodeIndex(try p.parseExpression(false))
469 optionalNodeIndex(try p.tryParseExpression(false))
460470 else
461471 .none;
462472
463473 _ = try p.expect(.equal);
464474
465 const data_init = try p.parseExpression(true);
475 const data_init = try p.tryParseExpression(true);
466476
467477 _ = try p.expect(.semi);
468478
......@@ -711,6 +721,13 @@ fn parseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex {
711721 return expr;
712722}
713723
724fn tryParseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex {
725 return if (p.isExpressionStart())
726 p.parseExpression(allow_assign_operators)
727 else
728 p.failUnexpectedToken(.expression);
729}
730
714731fn parseExpressionPart(p: *Parse) Error!NodeIndex {
715732 const trace = t.trace(@src());
716733 defer trace.end();
......@@ -1040,8 +1057,14 @@ fn fail(p: *Parse, ast_error: AstError) Error {
10401057}
10411058
10421059fn failUnexpectedToken(p: *Parse, kind: ExpectedContext) Error {
1060 var got = p.fullToken(p.pos);
1061 if (got.tag == .eof) {
1062 // note: it is impossible to get a failure at an empty file, so subtraction is fine
1063 const prev = p.fullToken(p.pos - 1);
1064 got.offset = prev.offset + prev.len(p.source);
1065 }
10431066 return p.fail(.{ .unexpected_context = .{
1044 .got = p.fullToken(p.pos),
1067 .got = got,
10451068 .expected = kind,
10461069 } });
10471070}
src/core/Report.zig+25-6
......@@ -247,10 +247,24 @@ pub const Err = union(enum) {
247247 node: NodeIndex,
248248
249249 fn build(data: @This(), ctx: *MsgContext) !void {
250 try ctx.msgNode(data.node, "invalid assignment target", .{});
251 }
252 },
253 bin_op_mismatch: struct {
254 src: LazyNode,
255 lhs: Type,
256 rhs: Type,
257
258 fn build(data: @This(), ctx: *MsgContext) !void {
259 const node = try data.src.get(ctx);
250260 try ctx.msgNode(
251 data.node,
252 "invalid assignment target",
253 .{},
261 node,
262 "\"{s}\" operator is not compatible with types \"{}\" and \"{}\"",
263 .{
264 ctx.ast.tokenSlice(ctx.ast.nodeToken(node)),
265 data.lhs.displayName(&ctx.comp.intern_pool),
266 data.rhs.displayName(&ctx.comp.intern_pool),
267 },
254268 );
255269 }
256270 },
......@@ -458,7 +472,13 @@ pub fn formatItem(
458472 const file = try comp.getFile(item.url.slice(report));
459473
460474 const need_load_ast = !(item_type == .@"error" and item.data.@"error" == .ast);
461 const ast_data = if (need_load_ast) try file.getAst(comp) else undefined;
475 const ast_data = if (need_load_ast)
476 try file.getAst(comp)
477 else brk: {
478 var ast: Ast = undefined;
479 ast.utf8_source = (try file.getSource(comp.gpa)).code;
480 break :brk ast;
481 };
462482
463483 var ctx: MsgContext = .{
464484 .arena = alloc,
......@@ -700,13 +720,12 @@ pub const Highlight = union(enum) {
700720 .byte = token.offset,
701721 };
702722 },
703
704723 .token_stored => |token| loc: {
705724 const location = Tokenizer.tokenLocation(ast.utf8_source, 0, token.offset);
706725
707726 break :loc .{
708727 .loc = location,
709 .slice = token.slice(ast.utf8_source),
728 .slice = if (token.tag != .eof) token.slice(ast.utf8_source) else "\n",
710729 .byte = token.offset,
711730 };
712731 },
src/core/Sema.zig+139-41
......@@ -315,28 +315,30 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref {
315315 .eq,
316316 => |tag| {
317317 const bin: Teg.Inst.Bin = s.code.instPayload(tag, in);
318 const op = @field(BinaryOp, @tagName(tag));
318319
319320 const lhs = try s.resolveRefRuntime(bin.lhs);
320321 const rhs = try s.resolveRefRuntime(bin.rhs);
321322
322 if (lhs.kind == .value and rhs.kind == .value) {
323 return (try s.comptimeBin(
324 @field(BinaryOp, @tagName(tag)),
325 lhs.decode().value,
326 rhs.decode().value,
327 ));
328 }
323 const left_ty = s.typeOfRef(lhs);
324 const right_ty = s.typeOfRef(rhs);
329325
330 const left_type = s.typeOfRef(lhs);
331 _ = left_type; // autofix
332 const right_type = s.typeOfRef(rhs);
333 _ = right_type; // autofix
326 const ty = try s.binTypeResolution(in, op, left_ty, right_ty);
334327
335 // TODO: validate type for operation
328 const left = try s.coerceRef(in.op(.lhs), lhs, ty);
329 const right = try s.coerceRef(in.op(.rhs), lhs, ty);
330
331 if (left.kind == .value and right.kind == .value) {
332 return (try s.comptimeBin(
333 op,
334 left.decode().value,
335 right.decode().value,
336 )).toAir();
337 }
336338
337339 return s.addInstRef(tag.airTag(), .{
338 .lhs = lhs,
339 .rhs = rhs,
340 .lhs = left,
341 .rhs = right,
340342 });
341343 },
342344 inline .negate, .bool_not => |tag| {
......@@ -881,35 +883,133 @@ const BinaryOp = enum {
881883 eq,
882884};
883885
884fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Air.Inst.Ref {
885 switch (op) {
886 .add => {
887 const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck / string add");
888 const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck / string add");
889 return (try s.ip.get(s.gpa, .{
890 .float = l + r,
891 })).toAir();
886fn binTypeResolution(s: *Sema, src: Teg.Inst.Index, op: BinaryOp, lhs: Type, rhs: Type) !Type {
887 if (lhs == .@"opaque") return rhs;
888 if (rhs == .@"opaque") return rhs;
889
890 switch (lhs) {
891 .string => if (op == .add) switch (rhs) {
892 .string => return .string,
893 else => {},
894 },
895 .numeric_literal => switch (rhs) {
896 .int32,
897 .float,
898 .numeric_literal,
899 => return rhs,
900 else => {},
901 },
902 .int32 => switch (rhs) {
903 .int32 => return .int32,
904 .numeric_literal => return .int32,
905 else => {},
906 },
907 .float => switch (rhs) {
908 .int32 => return .float,
909 .numeric_literal => return .float,
910 else => {},
892911 },
893 inline .sub, .mul, .div => |tag| {
894 const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck");
895 const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck");
912 else => {},
913 }
896914
897 const result = switch (tag) {
898 .sub => l - r,
899 .mul => l * r,
900 .div => l / r,
901 else => comptime unreachable,
902 };
915 return s.fail(.bin_op_mismatch, .{
916 .src = s.nodeLoc(src, .main_node),
917 .lhs = lhs,
918 .rhs = rhs,
919 });
920}
921
922fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Value {
923 std.debug.print("{} {s} {}\n", .{
924 lhs.dump(s.ip),
925 @tagName(op),
926 rhs.dump(s.ip),
927 });
928
929 const left_ty = lhs.typeOf(s.ip);
930 assert(rhs.typeOf(s.ip) == left_ty);
903931
904 return (try s.ip.get(s.gpa, .{
905 .float = result,
906 })).toAir();
932 switch (op) {
933 .add => switch (left_ty) {
934 .numeric_literal, .float => {
935 const l = lhs.toFloat(s.ip) orelse
936 unreachable; // must be coerced
937 const r = rhs.toFloat(s.ip) orelse
938 unreachable; // must be coerced
939
940 const result = l + r;
941
942 return try s.makeValue(if (left_ty == .numeric_literal)
943 .{ .comptime_float = result }
944 else
945 .{ .float = result });
946 },
947 .int32 => {
948 const l = lhs.toInt32(s.ip) orelse
949 unreachable; // must be coerced
950 const r = rhs.toInt32(s.ip) orelse
951 unreachable; // must be coerced
952
953 const result = std.math.add(i32, l, r) catch
954 @panic("TODO: out of range");
955
956 return try s.makeValue(.{ .int32 = result });
957 },
958 .string => {
959 const l = lhs.asString(s.ip).toSlice(s.ip);
960 const r = rhs.asString(s.ip).toSlice(s.ip);
961 var sfa = std.heap.stackFallback(4096, s.arena);
962 const alloc = sfa.get();
963 const joined = try std.mem.concat(alloc, u8, &.{ l, r });
964 defer alloc.free(joined);
965 return try s.makeValue(.{ .string = joined });
966 },
967 else => unreachable,
968 },
969 inline .sub, .mul, .div => |tag| switch (left_ty) {
970 .numeric_literal, .float => {
971 const l = lhs.toFloat(s.ip) orelse
972 unreachable; // must be coerced
973 const r = rhs.toFloat(s.ip) orelse
974 unreachable; // must be coerced
975
976 const result = switch (tag) {
977 .sub => l - r,
978 .mul => l * r,
979 .div => l / r,
980 else => comptime unreachable,
981 };
982
983 return try s.makeValue(if (left_ty == .numeric_literal)
984 .{ .comptime_float = result }
985 else
986 .{ .float = result });
987 },
988 .int32 => {
989 const l = lhs.toInt32(s.ip) orelse
990 unreachable; // must be coerced
991 const r = rhs.toInt32(s.ip) orelse
992 unreachable; // must be coerced
993
994 const result = switch (tag) {
995 .sub => std.math.sub(i32, l, r),
996 .mul => std.math.mul(i32, l, r),
997 .div => return try s.makeValue(.{ .float = @as(f64, @floatFromInt(l)) / @as(f64, @floatFromInt(r)) }),
998 else => comptime unreachable,
999 } catch @panic("TODO: out of range");
1000
1001 return try s.makeValue(.{ .int32 = result });
1002 },
1003 else => unreachable,
9071004 },
9081005 inline .lt,
9091006 .gt,
9101007 => |tag| {
911 const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck");
912 const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck");
1008 const l = lhs.toFloat(s.ip) orelse
1009 unreachable; // must be coerced
1010 const r = rhs.toFloat(s.ip) orelse
1011 unreachable; // must be coerced
1012
9131013 const result = switch (tag) {
9141014 .lt => l < r,
9151015 .gt => l > r,
......@@ -918,17 +1018,15 @@ fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Air.Inst.Ref {
9181018 return @as(
9191019 InternPool.Value,
9201020 if (result) .bool_true else .bool_false,
921 ).toAir();
1021 );
9221022 },
9231023 .eq,
9241024 => {
925 const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck / non-numeric eq");
926 const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck / non-numeric eq");
927 const result = l == r;
1025 const result = lhs == rhs;
9281026 return @as(
9291027 InternPool.Value,
9301028 if (result) .bool_true else .bool_false,
931 ).toAir();
1029 );
9321030 },
9331031 }
9341032}
src/core/Tokenizer.zig+2-1
......@@ -152,7 +152,6 @@ pub const Token = struct {
152152 .invalid => unreachable,
153153
154154 // do not have a static lexeme
155 .eof,
156155 .id,
157156 .builtin_id,
158157 .number_int,
......@@ -163,6 +162,8 @@ pub const Token = struct {
163162 .module_doc_comment,
164163 => null,
165164
165 .eof => unreachable, // always must be special cased
166
166167 .@"and" => "&",
167168 .and_and => "&&",
168169 .bang => "!",
src/langref/generate-langref.zig deleted-364
......@@ -1,364 +0,0 @@
1//! Language reference compiler. Converts markdown into HTML, using
2//! the Tokenizer to highlight code, and the compiler and node.js to
3//! show code results. Only ever built in Debug mode. We are not
4//! smart about memory. Written at 3am on a Thursday. Enjoy
5pub const std = @import("std");
6pub const core = @import("core");
7
8const ArrayList = std.ArrayList;
9
10pub fn main() !void {
11 comptime std.debug.assert(@import("builtin").mode == .Debug);
12 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
13 const gpa = arena.allocator();
14
15 var args = try std.process.argsWithAllocator(gpa);
16 _ = args.next().?;
17
18 const compiler_path = args.next().?;
19 const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
20 const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
21 const out_path = args.next().?;
22
23 var output = std.ArrayList(u8).init(gpa);
24 defer output.deinit();
25 try processMarkdown(
26 gpa,
27 markdown_src,
28 compiler_path,
29 output.writer(),
30 );
31
32 const applied_template = try std.mem.replaceOwned(
33 u8,
34 gpa,
35 template_src,
36 "{contents}",
37 output.items,
38 );
39 defer gpa.free(applied_template);
40
41 const out_file = try std.fs.createFileAbsolute(out_path, .{});
42 defer out_file.close();
43 try out_file.writeAll(applied_template);
44}
45
46const BlockType = enum {
47 para,
48 h1,
49 h2,
50 h3,
51 code_run,
52 code_build,
53 code,
54 code_generic,
55 ul,
56 table,
57 blockquote,
58};
59
60const InlineType = enum {
61 bold,
62 code,
63};
64
65const MarkdownTokenizer = struct {
66 /// Remaining text
67 str: []const u8,
68
69 fn skipWhitespace(t: *MarkdownTokenizer) void {
70 t.str = std.mem.trimLeft(u8, t.str, " \r\n");
71 }
72
73 fn advance(t: *MarkdownTokenizer, n: usize) []const u8 {
74 defer {
75 t.str = t.str[n..];
76 }
77 return t.str[0..n];
78 }
79
80 fn advanceLine(t: *MarkdownTokenizer) []const u8 {
81 return std.mem.trim(u8, t.advanceLineRaw(), " \r\n");
82 }
83
84 fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 {
85 const n = std.mem.indexOfScalar(u8, t.str, '\n');
86 const line = t.advance(if (n) |x| x + 1 else t.str.len);
87 return line;
88 }
89
90 fn blockType(t: *MarkdownTokenizer) BlockType {
91 switch (t.str[0]) {
92 '#' => {
93 defer {
94 while (t.str[0] == '#')
95 _ = t.advance(1);
96 }
97
98 if (t.str[1] != '#') return .h1;
99 if (t.str[2] != '#') return .h2;
100 if (t.str[3] != '#') return .h3;
101 @panic("headers nested too deep");
102 },
103 '>' => {
104 _ = t.advance(1);
105 return .blockquote;
106 },
107 '|' => {
108 return .table;
109 },
110 '`' => {
111 if (std.mem.startsWith(u8, t.str, "```")) {
112 defer _ = t.advanceLine();
113 if (std.mem.startsWith(u8, t.str, "```lang:run"))
114 return .code_run;
115 if (std.mem.startsWith(u8, t.str, "```lang:build"))
116 return .code_build;
117 if (std.mem.startsWith(u8, t.str, "```lang"))
118 return .code;
119 return .code_generic;
120 }
121 return .para;
122 },
123 '-' => {
124 return .ul;
125 },
126 else => {
127 return .para;
128 },
129 }
130 }
131};
132
133fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []const u8, writer: anytype) !void {
134 var t = MarkdownTokenizer{ .str = src };
135
136 t.skipWhitespace();
137 while (t.str.len > 0) : (t.skipWhitespace()) {
138 switch (t.blockType()) {
139 .para,
140 => {
141 try writer.writeAll("<p>\n");
142
143 var para = ArrayList(u8).init(gpa);
144 try para.appendSlice(t.advanceLine());
145
146 var next_line = t.advanceLine();
147 while (t.str.len > 0 and next_line.len > 0) {
148 try para.append(' ');
149 try para.appendSlice(next_line);
150 next_line = t.advanceLine();
151 }
152
153 try processInlineMarkdown(gpa, para.items, writer);
154
155 try writer.writeAll("\n</p>\n");
156 },
157 .h1,
158 .h2,
159 .h3,
160 => |tag| {
161 const html_tag = @tagName(tag);
162 const name = t.advanceLine();
163 try writer.print("<{0s}>{1s}</{0s}>\n", .{
164 html_tag,
165 name,
166 });
167 },
168 .code_run,
169 .code_build,
170 .code,
171 .code_generic,
172 => |tag| {
173 var code = ArrayList(u8).init(gpa);
174
175 var next_line = t.advanceLineRaw();
176 while (t.str.len > 0 and !std.mem.startsWith(u8, next_line, "```")) {
177 try code.appendSlice(next_line);
178 next_line = t.advanceLineRaw();
179 }
180
181 try writer.writeAll("<pre><code>");
182 try writeTokenizedCode(gpa, code.items, writer);
183 try writer.writeAll("</code></pre>");
184
185 try code.append('\n');
186
187 switch (tag) {
188 .code_run,
189 .code_build,
190 => {
191 var build = std.process.Child.init(&.{ compiler_path, "/dev/stdin" }, gpa);
192 build.stdin_behavior = .Pipe;
193 build.stdout_behavior = .Pipe;
194 build.stderr_behavior = .Pipe;
195 var env_map = try std.process.getEnvMap(gpa);
196 defer env_map.deinit();
197 try env_map.put("YES_COLOR", "1");
198 build.env_map = &env_map;
199 try build.spawn();
200 try build.stdin.?.writeAll(code.items);
201 var out = ArrayList(u8).init(gpa);
202 var err = ArrayList(u8).init(gpa);
203 try build.collectOutput(&out, &err, std.math.maxInt(usize));
204 const result = try build.wait();
205 _ = result;
206 if (std.mem.trim(u8, err.items, " \n\r").len > 0) {
207 try writer.writeAll("<pre class='term'><code>");
208 try writeAnsi(gpa, err.items, writer);
209 try writer.writeAll("</code></pre>");
210 }
211 if (std.mem.trim(u8, out.items, " \n\r").len > 0) {
212 try writer.writeAll("<pre><code>");
213 try writer.writeAll("// Compiled JavaScript Code");
214 try writer.writeAll(out.items[std.mem.indexOfScalar(u8, out.items, '\n').?..]);
215 try writer.writeAll("</code></pre>");
216 }
217 },
218 else => {},
219 }
220 },
221 .ul,
222 => {
223 try writer.writeAll("<ul>");
224 while (t.str.len > 0 and (t.str[0] == '-')) {
225 _ = t.advance(1);
226 const line = t.advanceLine();
227 try writer.writeAll("<li>");
228 try processInlineMarkdown(gpa, line, writer);
229 try writer.writeAll("</li>");
230 }
231 try writer.writeAll("</ul>");
232 },
233 .table,
234 => {
235 _ = t.advanceLine();
236 },
237 .blockquote,
238 => {
239 try writer.writeAll("<blockquote>\n");
240
241 var para = ArrayList(u8).init(gpa);
242 try para.appendSlice(t.advanceLine());
243
244 var next_line = t.advanceLine();
245 while (t.str.len > 0 and next_line.len > 0) {
246 try para.append(' ');
247 try para.appendSlice(next_line);
248 next_line = t.advanceLine();
249 }
250
251 try processInlineMarkdown(gpa, para.items, writer);
252
253 try writer.writeAll("\n</blockquote>\n");
254 },
255 }
256 }
257}
258
259pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
260 var pos: usize = 0;
261 while (true) : (pos += 1) {
262 pos = std.mem.indexOfAnyPos(u8, src, pos, "*`") orelse return null;
263
264 switch (src[pos]) {
265 '*' => switch (src[pos + 1]) {
266 '*' => return .{ .bold, pos },
267 else => continue,
268 },
269 '`' => return .{ .code, pos },
270 else => unreachable,
271 }
272 }
273}
274
275fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytype) !void {
276 var bold = false;
277 var remain = src;
278
279 while (nextInlineToken(remain)) |tok| {
280 try writer.writeAll(remain[0..tok[1]]);
281 remain = remain[tok[1]..];
282 switch (tok[0]) {
283 .bold => {
284 bold = !bold;
285 try writer.writeAll(switch (bold) {
286 true => "<strong>",
287 false => "</strong>",
288 });
289 remain = remain[2..];
290 },
291 .code => {
292 const end = std.mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
293 const code = remain[1..end];
294 try writer.writeAll("<code>");
295 try writeTokenizedCode(gpa, code, writer);
296 try writer.writeAll("</code>");
297 remain = remain[end + 1 ..];
298 },
299 }
300 }
301 try writer.writeAll(remain);
302
303 if (bold) {
304 std.debug.panic("Unterminated Bold in '{s}'", .{src});
305 }
306}
307
308// TODO: Change this to use Tree-sitter, which would also make it very simple
309// to highlight JavaScript code in addition to the language.
310fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
311 const code_z = try gpa.dupeZ(u8, code);
312 defer gpa.free(code_z);
313
314 var t = core.Tokenizer{ .code = code_z };
315
316 t.skipWhitespace();
317 var last: usize = 0;
318 while (true) {
319 t.skipWhitespace();
320 const tok = t.next() catch {
321 t.pos += 1;
322 continue;
323 } orelse break;
324 try writer.writeAll(t.code[last..tok.offset]);
325 try writer.print("<span class='tok-{s}{s}'>{s}</span>", .{
326 @tagName(tok.tag),
327 if (tok.tag.isKeyword()) " kw" else "",
328 t.code[tok.offset..t.pos],
329 });
330 last = t.pos;
331 }
332}
333
334fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
335 var remain = code;
336 var count: usize = 0;
337 while (std.mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
338 try writer.writeAll(remain[0..esc_i]);
339 switch (remain[esc_i + 1]) {
340 '[' => switch (remain[esc_i + 2]) {
341 '0'...'9' => {
342 const end = std.mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
343 const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
344 switch (int) {
345 else => {
346 try writer.print("<span class='ansi-{d}'>", .{int});
347 count += 1;
348 },
349 0 => {
350 for (0..count) |_| {
351 try writer.writeAll("</span>");
352 }
353 count = 0;
354 },
355 }
356 remain = remain[end + 1 ..];
357 },
358 else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
359 },
360 else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
361 }
362 }
363 _ = gpa;
364}
src/langref/template.html deleted-100
......@@ -1,100 +0,0 @@
1<!doctype html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Language Reference</title>
7 <style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9 body {
10 background-color: #fefefe;
11 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
12 max-width: 45rem;
13 padding: 1rem;
14 margin: auto;
15 }
16 h1,h2,h3,h4,h5,h6,p,ul,pre,blockquote {
17 margin-bottom: 1rem;
18 }
19 ul {
20 padding-left: 2rem;
21 }
22 li {
23 margin-bottom: 0.25rem;
24 }
25 blockquote {
26 background-color: #ddd;
27 border-left: 4px solid black;
28 padding-left: 16px;
29 padding-top: 8px;
30 padding-bottom: 8px;
31 }
32 pre > code {
33 border: none;
34 padding: 0;
35 background-color: transparent;
36 }
37 code, pre {
38 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
39 font-size: 0.9rem;
40 border: 1px solid #9ca0b0;
41 background-color: #eff1f5;
42 border-radius: 4px;
43 padding: 2px 4px;
44 }
45 pre {
46 padding: 0.75rem 0.75rem;
47 }
48 .term {
49 border: 1px solid #11111b;
50 background-color: #24273a;
51 color: #cdd6f4;
52 }
53
54 .ansi-1 {
55 font-weight: bold;
56 }
57 .ansi-31 {
58 color: #ed8796;
59 }
60 .ansi-32 {
61 color: #a6e3a1;
62 }
63 .ansi-33 {
64 color: #eed49f;
65 }
66 .ansi-34 {
67 color: #8aadf4;
68 }
69 .ansi-35 {
70 color: #f5c2e7;
71 }
72 .ansi-36 {
73 color: #94e2d5;
74 }
75 .ansi-2 {
76 color: #8087a2;
77 }
78
79 .kw {
80 color: #8839ef;
81 font-weight: bold;
82 }
83 .tok-string {
84 color: #40a02b;
85 }
86 .tok-builtin_id {
87 color: #d20f39;
88 font-weight: bold;
89 }
90 .tok-number_int, .tok-number_float {
91 color: #fe640b;
92 }
93 </style>
94</head>
95<body>
96 <main>
97 {contents}
98 </main>
99</body>
100</html>
src/manual/generate-man.zig created+387
......@@ -0,0 +1,387 @@
1//! Language reference compiler. Converts markdown into HTML, using
2//! the Tokenizer to highlight code, and the compiler and node.js to
3//! show code results. Only ever built in Debug mode. We are not
4//! smart about memory. Written at 3am on a Thursday. Enjoy
5pub const std = @import("std");
6pub const core = @import("core");
7
8const ArrayList = std.ArrayList;
9
10pub fn main() !void {
11 comptime std.debug.assert(@import("builtin").mode == .Debug);
12 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
13 const gpa = arena.allocator();
14
15 var args = try std.process.argsWithAllocator(gpa);
16 _ = args.next().?;
17
18 const compiler_path = args.next().?;
19 const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
20 const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize));
21 const out_path = args.next().?;
22
23 var output = std.ArrayList(u8).init(gpa);
24 defer output.deinit();
25 try processMarkdown(
26 gpa,
27 markdown_src,
28 compiler_path,
29 output.writer(),
30 );
31
32 const applied_template = try std.mem.replaceOwned(
33 u8,
34 gpa,
35 template_src,
36 "{contents}",
37 output.items,
38 );
39 defer gpa.free(applied_template);
40
41 const out_file = try std.fs.createFileAbsolute(out_path, .{});
42 defer out_file.close();
43 try out_file.writeAll(applied_template);
44}
45
46const BlockType = enum {
47 para,
48 h1,
49 h2,
50 h3,
51 code_run,
52 code_build,
53 code,
54 code_generic,
55 ul,
56 table,
57 blockquote,
58};
59
60const InlineType = enum {
61 bold,
62 code,
63};
64
65const MarkdownTokenizer = struct {
66 /// Remaining text
67 str: []const u8,
68
69 fn skipWhitespace(t: *MarkdownTokenizer) void {
70 t.str = std.mem.trimLeft(u8, t.str, " \r\n");
71 }
72
73 fn advance(t: *MarkdownTokenizer, n: usize) []const u8 {
74 defer {
75 t.str = t.str[n..];
76 }
77 return t.str[0..n];
78 }
79
80 fn advanceLine(t: *MarkdownTokenizer) []const u8 {
81 return std.mem.trim(u8, t.advanceLineRaw(), " \r\n");
82 }
83
84 fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 {
85 const n = std.mem.indexOfScalar(u8, t.str, '\n');
86 const line = t.advance(if (n) |x| x + 1 else t.str.len);
87 return line;
88 }
89
90 fn blockType(t: *MarkdownTokenizer) BlockType {
91 switch (t.str[0]) {
92 '#' => {
93 defer {
94 while (t.str[0] == '#')
95 _ = t.advance(1);
96 }
97
98 if (t.str[1] != '#') return .h1;
99 if (t.str[2] != '#') return .h2;
100 if (t.str[3] != '#') return .h3;
101 @panic("headers nested too deep");
102 },
103 '>' => {
104 _ = t.advance(1);
105 return .blockquote;
106 },
107 '|' => {
108 return .table;
109 },
110 '`' => {
111 if (std.mem.startsWith(u8, t.str, "```")) {
112 defer _ = t.advanceLine();
113 if (std.mem.startsWith(u8, t.str, "```lang:run"))
114 return .code_run;
115 if (std.mem.startsWith(u8, t.str, "```lang:build"))
116 return .code_build;
117 if (std.mem.startsWith(u8, t.str, "```lang"))
118 return .code;
119 return .code_generic;
120 }
121 return .para;
122 },
123 '-' => {
124 return .ul;
125 },
126 else => {
127 return .para;
128 },
129 }
130 }
131};
132
133fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []const u8, writer: anytype) !void {
134 var t = MarkdownTokenizer{ .str = src };
135
136 t.skipWhitespace();
137 while (t.str.len > 0) : (t.skipWhitespace()) {
138 switch (t.blockType()) {
139 .para,
140 => {
141 try writer.writeAll("<p>\n");
142
143 var para = ArrayList(u8).init(gpa);
144 try para.appendSlice(t.advanceLine());
145
146 var next_line = t.advanceLine();
147 while (t.str.len > 0 and next_line.len > 0) {
148 try para.append(' ');
149 try para.appendSlice(next_line);
150 next_line = t.advanceLine();
151 }
152
153 try processInlineMarkdown(gpa, para.items, writer);
154
155 try writer.writeAll("\n</p>\n");
156 },
157 .h1,
158 .h2,
159 .h3,
160 => |tag| {
161 const html_tag = @tagName(tag);
162 const name = t.advanceLine();
163 try writer.print("<{0s}>{1s}</{0s}>\n", .{
164 html_tag,
165 name,
166 });
167 },
168 .code_run,
169 .code_build,
170 .code,
171 .code_generic,
172 => |tag| {
173 var code = ArrayList(u8).init(gpa);
174
175 var next_line = t.advanceLineRaw();
176 while (t.str.len > 0 and !std.mem.startsWith(u8, next_line, "```")) {
177 try code.appendSlice(next_line);
178 next_line = t.advanceLineRaw();
179 }
180
181 try writer.writeAll("<pre><code>");
182 try writeTokenizedCode(gpa, code.items, writer);
183 try writer.writeAll("</code></pre>");
184
185 try code.append('\n');
186
187 switch (tag) {
188 .code_run,
189 .code_build,
190 => {
191 var build = std.process.Child.init(&.{ compiler_path, "/dev/stdin" }, gpa);
192 build.stdin_behavior = .Pipe;
193 build.stdout_behavior = .Pipe;
194 build.stderr_behavior = .Pipe;
195 var env_map = try std.process.getEnvMap(gpa);
196 defer env_map.deinit();
197 try env_map.put("YES_COLOR", "1");
198 build.env_map = &env_map;
199 try build.spawn();
200 try build.stdin.?.writeAll(code.items);
201 var out = ArrayList(u8).init(gpa);
202 var err = ArrayList(u8).init(gpa);
203 try build.collectOutput(&out, &err, std.math.maxInt(usize));
204 const result = try build.wait();
205 _ = result;
206 if (std.mem.trim(u8, err.items, " \n\r").len > 0) {
207 try writer.writeAll("<pre class='term'><code>");
208 try writeAnsi(gpa, err.items, writer);
209 try writer.writeAll("</code></pre>");
210 }
211 if (std.mem.trim(u8, out.items, " \n\r").len > 0) {
212 try writer.writeAll("<pre><code>");
213 try writer.writeAll("// Compiled JavaScript Code");
214 try writer.writeAll(out.items[std.mem.indexOfScalar(u8, out.items, '\n').?..]);
215 try writer.writeAll("</code></pre>");
216 }
217 },
218 else => {},
219 }
220 },
221 .ul,
222 => {
223 try writer.writeAll("<ul>");
224 while (t.str.len > 0 and (t.str[0] == '-')) {
225 _ = t.advance(1);
226 const line = t.advanceLine();
227 try writer.writeAll("<li>");
228 try processInlineMarkdown(gpa, line, writer);
229 try writer.writeAll("</li>");
230 }
231 try writer.writeAll("</ul>");
232 },
233 .table,
234 => {
235 try writer.writeAll("<table><tbody>");
236 {
237 try writer.writeAll("<tr>");
238 var it = std.mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
239 while (it.next()) |tok| {
240 try writer.writeAll("<th>");
241 try processInlineMarkdown(gpa, tok, writer);
242 try writer.writeAll("</th>");
243 }
244 try writer.writeAll("</tr>");
245 }
246 _ = t.advanceLine();
247 t.skipWhitespace();
248 while (t.str.len > 0 and t.str[0] == '|') {
249 try writer.writeAll("<tr>");
250 var it = std.mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
251 while (it.next()) |tok| {
252 try writer.writeAll("<td>");
253 try processInlineMarkdown(gpa, tok, writer);
254 try writer.writeAll("</td>");
255 }
256 try writer.writeAll("</tr>");
257 }
258 try writer.writeAll("</tbody></table>");
259 },
260 .blockquote,
261 => {
262 try writer.writeAll("<blockquote>\n");
263
264 var para = ArrayList(u8).init(gpa);
265 try para.appendSlice(t.advanceLine());
266
267 var next_line = t.advanceLine();
268 while (t.str.len > 0 and next_line.len > 0) {
269 try para.append(' ');
270 try para.appendSlice(next_line);
271 next_line = t.advanceLine();
272 }
273
274 try processInlineMarkdown(gpa, para.items, writer);
275
276 try writer.writeAll("\n</blockquote>\n");
277 },
278 }
279 }
280}
281
282pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
283 var pos: usize = 0;
284 while (true) : (pos += 1) {
285 pos = std.mem.indexOfAnyPos(u8, src, pos, "*`") orelse return null;
286
287 switch (src[pos]) {
288 '*' => switch (src[pos + 1]) {
289 '*' => return .{ .bold, pos },
290 else => continue,
291 },
292 '`' => return .{ .code, pos },
293 else => unreachable,
294 }
295 }
296}
297
298fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytype) !void {
299 var bold = false;
300 var remain = src;
301
302 while (nextInlineToken(remain)) |tok| {
303 try writer.writeAll(remain[0..tok[1]]);
304 remain = remain[tok[1]..];
305 switch (tok[0]) {
306 .bold => {
307 bold = !bold;
308 try writer.writeAll(switch (bold) {
309 true => "<strong>",
310 false => "</strong>",
311 });
312 remain = remain[2..];
313 },
314 .code => {
315 const end = std.mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
316 const code = remain[1..end];
317 try writer.writeAll("<code>");
318 try writeTokenizedCode(gpa, code, writer);
319 try writer.writeAll("</code>");
320 remain = remain[end + 1 ..];
321 },
322 }
323 }
324 try writer.writeAll(remain);
325
326 if (bold) {
327 std.debug.panic("Unterminated Bold in '{s}'", .{src});
328 }
329}
330
331// TODO: Change this to use Tree-sitter, which would also make it very simple
332// to highlight JavaScript code in addition to the language.
333fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
334 const code_z = try gpa.dupeZ(u8, code);
335 defer gpa.free(code_z);
336
337 var t = core.Tokenizer{ .code = code_z };
338
339 t.skipWhitespace();
340 var last: usize = 0;
341 while (true) {
342 t.skipWhitespace();
343 const tok = t.next() catch {
344 t.pos += 1;
345 continue;
346 } orelse break;
347 try writer.writeAll(t.code[last..tok.offset]);
348 try writer.print("<span class='tok-{s}{s}'>{s}</span>", .{
349 @tagName(tok.tag),
350 if (tok.tag.isKeyword()) " kw" else "",
351 t.code[tok.offset..t.pos],
352 });
353 last = t.pos;
354 }
355}
356
357fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
358 var remain = code;
359 var count: usize = 0;
360 while (std.mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
361 try writer.writeAll(remain[0..esc_i]);
362 switch (remain[esc_i + 1]) {
363 '[' => switch (remain[esc_i + 2]) {
364 '0'...'9' => {
365 const end = std.mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
366 const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
367 switch (int) {
368 else => {
369 try writer.print("<span class='ansi-{d}'>", .{int});
370 count += 1;
371 },
372 0 => {
373 for (0..count) |_| {
374 try writer.writeAll("</span>");
375 }
376 count = 0;
377 },
378 }
379 remain = remain[end + 1 ..];
380 },
381 else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
382 },
383 else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
384 }
385 }
386 _ = gpa;
387}
src/manual/template.html created+134
......@@ -0,0 +1,134 @@
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Language Reference</title>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13 body {
14 background-color: #fefefe;
15 font-family:
16 system-ui,
17 -apple-system,
18 BlinkMacSystemFont,
19 "Segoe UI",
20 Roboto,
21 Oxygen,
22 Ubuntu,
23 Cantarell,
24 "Open Sans",
25 "Helvetica Neue",
26 sans-serif;
27 max-width: 45rem;
28 padding: 1rem;
29 margin: auto;
30 }
31 h1,
32 h2,
33 h3,
34 h4,
35 h5,
36 h6,
37 p,
38 ul,
39 pre,
40 blockquote,
41 table {
42 margin-bottom: 1rem;
43 }
44 ul {
45 padding-left: 2rem;
46 }
47 li {
48 margin-bottom: 0.25rem;
49 }
50 blockquote {
51 background-color: #ddd;
52 border-left: 4px solid black;
53 padding-left: 16px;
54 padding-top: 8px;
55 padding-bottom: 8px;
56 }
57 pre > code {
58 border: none;
59 padding: 0;
60 background-color: transparent;
61 }
62 code,
63 pre {
64 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
65 "Liberation Mono", "Courier New", monospace;
66 font-size: 0.9rem;
67 border: 1px solid #9ca0b0;
68 background-color: #eff1f5;
69 border-radius: 4px;
70 padding: 2px 4px;
71 }
72 pre {
73 padding: 0.75rem 0.75rem;
74 }
75 .term {
76 border: 1px solid #11111b;
77 background-color: #24273a;
78 color: #cdd6f4;
79 }
80 table,
81 th,
82 td {
83 border: 1px solid #9ca0b0;
84 border-collapse: collapse;
85 }
86 th,
87 td {
88 padding: 0.5rem;
89 }
90 .ansi-1 {
91 font-weight: bold;
92 }
93 .ansi-31 {
94 color: #ed8796;
95 }
96 .ansi-32 {
97 color: #a6e3a1;
98 }
99 .ansi-33 {
100 color: #eed49f;
101 }
102 .ansi-34 {
103 color: #8aadf4;
104 }
105 .ansi-35 {
106 color: #f5c2e7;
107 }
108 .ansi-36 {
109 color: #94e2d5;
110 }
111 .ansi-2 {
112 color: #8087a2;
113 }
114 .kw {
115 color: #8839ef;
116 font-weight: bold;
117 }
118 .tok-string {
119 color: #40a02b;
120 }
121 .tok-builtin_id {
122 color: #d20f39;
123 font-weight: bold;
124 }
125 .tok-number_int,
126 .tok-number_float {
127 color: #fe640b;
128 }
129 </style>
130 </head>
131 <body>
132 <main>{contents}</main>
133 </body>
134</html>